openxiangda 1.0.50 → 1.0.51
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/lib/cli.js +339 -9
- package/lib/workspace-init.js +20 -8
- package/openxiangda-skills/SKILL.md +4 -3
- package/openxiangda-skills/skills/openxiangda-app/SKILL.md +28 -0
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +45 -1
- package/openxiangda-skills/skills/openxiangda-permission-settings/SKILL.md +31 -0
- package/package.json +7 -1
- package/packages/sdk/dist/runtime/index.cjs +3590 -3376
- package/packages/sdk/dist/runtime/index.cjs.map +1 -1
- package/packages/sdk/dist/runtime/index.d.mts +1 -0
- package/packages/sdk/dist/runtime/index.d.ts +1 -0
- package/packages/sdk/dist/runtime/index.mjs +3079 -2859
- package/packages/sdk/dist/runtime/index.mjs.map +1 -1
- package/packages/sdk/dist/runtime/react.cjs +236 -0
- package/packages/sdk/dist/runtime/react.cjs.map +1 -0
- package/packages/sdk/dist/runtime/react.d.mts +109 -0
- package/packages/sdk/dist/runtime/react.d.ts +109 -0
- package/packages/sdk/dist/runtime/react.mjs +222 -0
- package/packages/sdk/dist/runtime/react.mjs.map +1 -0
- package/templates/openxiangda-react-spa/.env.example +4 -0
- package/templates/openxiangda-react-spa/AGENTS.md +65 -0
- package/templates/openxiangda-react-spa/index.html +12 -0
- package/templates/openxiangda-react-spa/package.json +35 -0
- package/templates/openxiangda-react-spa/postcss.config.cjs +6 -0
- package/templates/openxiangda-react-spa/src/app/router.tsx +97 -0
- package/templates/openxiangda-react-spa/src/layouts/AdminShell.tsx +102 -0
- package/templates/openxiangda-react-spa/src/layouts/PublicShell.tsx +11 -0
- package/templates/openxiangda-react-spa/src/layouts/UserShell.tsx +22 -0
- package/templates/openxiangda-react-spa/src/main.tsx +12 -0
- package/templates/openxiangda-react-spa/src/pages/admin/RuntimeWorkspacePage.tsx +57 -0
- package/templates/openxiangda-react-spa/src/pages/defaults/DataRoutePage.tsx +17 -0
- package/templates/openxiangda-react-spa/src/pages/defaults/FilePreviewRoutePage.tsx +14 -0
- package/templates/openxiangda-react-spa/src/pages/defaults/FormRoutePage.tsx +62 -0
- package/templates/openxiangda-react-spa/src/pages/portal/UserPortalPage.tsx +27 -0
- package/templates/openxiangda-react-spa/src/pages/public/PublicHomePage.tsx +10 -0
- package/templates/openxiangda-react-spa/src/pages/states/NotFoundPage.tsx +16 -0
- package/templates/openxiangda-react-spa/src/resources/menus/menus.json +31 -0
- package/templates/openxiangda-react-spa/src/resources/permissions/page-groups/app-admin.json +8 -0
- package/templates/openxiangda-react-spa/src/resources/permissions/page-groups/app-user.json +8 -0
- package/templates/openxiangda-react-spa/src/resources/roles/roles.json +14 -0
- package/templates/openxiangda-react-spa/src/styles/index.css +23 -0
- package/templates/openxiangda-react-spa/tailwind.config.cjs +29 -0
- package/templates/openxiangda-react-spa/tsconfig.app.json +36 -0
- package/templates/openxiangda-react-spa/tsconfig.json +7 -0
- package/templates/openxiangda-react-spa/tsconfig.node.json +10 -0
- package/templates/openxiangda-react-spa/vite.config.ts +73 -0
package/lib/cli.js
CHANGED
|
@@ -59,6 +59,7 @@ async function main(argv) {
|
|
|
59
59
|
if (command === 'permission') return permission(rest);
|
|
60
60
|
if (command === 'settings') return settings(rest);
|
|
61
61
|
if (command === 'resource') return resource(rest);
|
|
62
|
+
if (command === 'runtime') return runtime(rest);
|
|
62
63
|
if (command === 'inspect') return inspect(rest);
|
|
63
64
|
if (command === 'skill') return skill(rest);
|
|
64
65
|
if (command === 'feedback') return feedback(rest);
|
|
@@ -78,12 +79,12 @@ Usage:
|
|
|
78
79
|
openxiangda platform use <name>
|
|
79
80
|
openxiangda auth status|refresh|logout [--profile name]
|
|
80
81
|
openxiangda env [--profile name]
|
|
81
|
-
openxiangda workspace init [dir] [--name package-name] [--install] [--profile name --app-type APP_XXX]
|
|
82
|
-
openxiangda workspace init [dir] --profile <name> --app-name <app-name> [--install]
|
|
82
|
+
openxiangda workspace init [dir] [--name package-name] [--runtime legacy|react-spa] [--install] [--profile name --app-type APP_XXX]
|
|
83
|
+
openxiangda workspace init [dir] --profile <name> --app-name <app-name> [--runtime legacy|react-spa] [--install]
|
|
83
84
|
openxiangda workspace bind --profile <name> --app-type <APP_XXX>
|
|
84
85
|
openxiangda workspace publish --profile <name> [--changed [--since ref]|--form code|--page code|--only list] [--dry-run] [--force] [--resources|--skip-resources] [--prune]
|
|
85
86
|
openxiangda app list [--profile name] [--json]
|
|
86
|
-
openxiangda app create <name> [--profile name] [--description text]
|
|
87
|
+
openxiangda app create <name> [--profile name] [--runtime legacy|react-spa] [--description text]
|
|
87
88
|
openxiangda app snapshot <APP_XXX> [--profile name] [--json]
|
|
88
89
|
openxiangda form list [--profile name] [--json]
|
|
89
90
|
openxiangda form create <formCode> [--name text] [--type receipt] # low-level shell only
|
|
@@ -113,6 +114,9 @@ Usage:
|
|
|
113
114
|
openxiangda permission form-group-list|form-group-create|form-group-bind
|
|
114
115
|
openxiangda settings get|save|indexes|indexes-save|data-management|data-management-save|public-access
|
|
115
116
|
openxiangda resource validate|plan|publish|pull [--profile name] [--json]
|
|
117
|
+
openxiangda runtime deploy [--profile name] [--dist dist] [--build-id id] [--no-build] [--no-activate] [--json]
|
|
118
|
+
openxiangda runtime releases [--profile name] [--json]
|
|
119
|
+
openxiangda runtime activate <releaseId> [--profile name] [--json]
|
|
116
120
|
openxiangda inspect app|form|workflow|automation|permissions
|
|
117
121
|
openxiangda feedback preview|submit --summary <text> [--type bug] [--severity medium] [--profile name] [--yes]
|
|
118
122
|
openxiangda skill install [--agent codex|claude|qoder|dual] [--dest <skills-dir>] [--force] [--dry-run] [--json]
|
|
@@ -1040,11 +1044,13 @@ async function workspace(args) {
|
|
|
1040
1044
|
assertCanInitializeWorkspace({
|
|
1041
1045
|
dir: positional[0],
|
|
1042
1046
|
force: flags.force,
|
|
1047
|
+
runtime: flags.runtime || flags.template,
|
|
1043
1048
|
});
|
|
1044
1049
|
const data = await createPlatformApp(config, profileName, {
|
|
1045
1050
|
name: flags['app-name'],
|
|
1046
1051
|
description: flags.description || '',
|
|
1047
1052
|
iconfontCss: flags['iconfont-css'] || '',
|
|
1053
|
+
runtimeMode: normalizeWorkspaceRuntimeFlag(flags.runtime || flags.template),
|
|
1048
1054
|
});
|
|
1049
1055
|
appType = extractCreatedAppType(data);
|
|
1050
1056
|
if (!appType) {
|
|
@@ -1064,6 +1070,7 @@ async function workspace(args) {
|
|
|
1064
1070
|
force: flags.force,
|
|
1065
1071
|
profile: profileName,
|
|
1066
1072
|
appType,
|
|
1073
|
+
runtime: flags.runtime || flags.template,
|
|
1067
1074
|
});
|
|
1068
1075
|
if (createdApp) {
|
|
1069
1076
|
result.createdApp = createdApp;
|
|
@@ -1167,6 +1174,7 @@ async function app(args) {
|
|
|
1167
1174
|
name,
|
|
1168
1175
|
description: flags.description || '',
|
|
1169
1176
|
iconfontCss: flags['iconfont-css'] || '',
|
|
1177
|
+
runtimeMode: normalizeWorkspaceRuntimeFlag(flags.runtime || flags.template),
|
|
1170
1178
|
});
|
|
1171
1179
|
if (flags.json) return writeJson(data);
|
|
1172
1180
|
print(JSON.stringify(data, null, 2));
|
|
@@ -1194,10 +1202,16 @@ async function createPlatformApp(config, profileName, payload) {
|
|
|
1194
1202
|
name: payload.name,
|
|
1195
1203
|
description: payload.description || '',
|
|
1196
1204
|
iconfontCss: payload.iconfontCss || '',
|
|
1205
|
+
...(payload.runtimeMode ? { runtimeMode: payload.runtimeMode } : {}),
|
|
1206
|
+
...(payload.runtimeSettings ? { runtimeSettings: payload.runtimeSettings } : {}),
|
|
1197
1207
|
},
|
|
1198
1208
|
});
|
|
1199
1209
|
}
|
|
1200
1210
|
|
|
1211
|
+
function normalizeWorkspaceRuntimeFlag(value) {
|
|
1212
|
+
return value === 'react-spa' || value === 'spa' ? 'react-spa' : undefined;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1201
1215
|
function extractCreatedAppType(data) {
|
|
1202
1216
|
const candidates = [
|
|
1203
1217
|
data?.appType,
|
|
@@ -2215,11 +2229,14 @@ async function permission(args) {
|
|
|
2215
2229
|
const [groupCode] = positional;
|
|
2216
2230
|
const name = flags.name || positional.slice(1).join(' ') || groupCode;
|
|
2217
2231
|
if (!groupCode || !name) {
|
|
2218
|
-
fail('用法: openxiangda permission page-group-create <groupCode> --name <text> [--menu-codes <menuCode...>|--page-codes <pageCode...>|--form-codes <formCode...>|--menu-ids <id...>] [--no-runtime-aliases]');
|
|
2232
|
+
fail('用法: openxiangda permission page-group-create <groupCode> --name <text> [--menu-codes <menuCode...>|--route-codes <routeCode...>|--path-patterns <pattern...>|--page-codes <pageCode...>|--form-codes <formCode...>|--menu-ids <id...>] [--no-runtime-aliases]');
|
|
2219
2233
|
}
|
|
2220
2234
|
const target = getWorkspaceTarget(config, profileName, flags);
|
|
2221
2235
|
const roles = splitList(flags.roles);
|
|
2222
2236
|
const editableTargets = resolvePagePermissionMenuTargets(target.bound, flags);
|
|
2237
|
+
const menuCodes = splitList(flags['menu-codes']);
|
|
2238
|
+
const routeCodes = splitList(flags['route-codes']);
|
|
2239
|
+
const pathPatterns = splitList(flags['path-patterns']);
|
|
2223
2240
|
const runtimeAliasTargets = flags['no-runtime-aliases']
|
|
2224
2241
|
? []
|
|
2225
2242
|
: resolvePagePermissionRuntimeAliasTargets(target.bound, flags);
|
|
@@ -2233,6 +2250,9 @@ async function permission(args) {
|
|
|
2233
2250
|
name,
|
|
2234
2251
|
roles,
|
|
2235
2252
|
menuFormUuids: editableTargets,
|
|
2253
|
+
menuCodes,
|
|
2254
|
+
routeCodes,
|
|
2255
|
+
pathPatterns,
|
|
2236
2256
|
},
|
|
2237
2257
|
}
|
|
2238
2258
|
);
|
|
@@ -2564,8 +2584,8 @@ async function resource(args) {
|
|
|
2564
2584
|
const config = loadConfig();
|
|
2565
2585
|
const profileName = flags.profile || config.currentProfile;
|
|
2566
2586
|
|
|
2567
|
-
if (!['validate', 'plan', 'publish', 'pull'].includes(subcommand)) {
|
|
2568
|
-
fail('用法: openxiangda resource validate|plan|publish|pull [--profile name] [--json]');
|
|
2587
|
+
if (!['validate', 'plan', 'publish', 'pull', 'typegen'].includes(subcommand)) {
|
|
2588
|
+
fail('用法: openxiangda resource validate|plan|publish|pull|typegen [--profile name] [--json]');
|
|
2569
2589
|
}
|
|
2570
2590
|
|
|
2571
2591
|
if (subcommand === 'pull') {
|
|
@@ -2577,6 +2597,13 @@ async function resource(args) {
|
|
|
2577
2597
|
}
|
|
2578
2598
|
|
|
2579
2599
|
const manifest = loadWorkspaceResources();
|
|
2600
|
+
if (subcommand === 'typegen') {
|
|
2601
|
+
const result = generateResourceTypes(manifest, flags.out || flags.output);
|
|
2602
|
+
if (flags.json) return writeJson(result);
|
|
2603
|
+
print(`已生成资源类型: ${result.filePath}`);
|
|
2604
|
+
return;
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2580
2607
|
const validation = validateWorkspaceResources(manifest);
|
|
2581
2608
|
if (subcommand === 'validate') {
|
|
2582
2609
|
if (flags.json) return writeJson(validation);
|
|
@@ -2605,6 +2632,226 @@ async function resource(args) {
|
|
|
2605
2632
|
printResourceResult(result);
|
|
2606
2633
|
}
|
|
2607
2634
|
|
|
2635
|
+
async function runtime(args) {
|
|
2636
|
+
const [subcommand, ...rest] = args;
|
|
2637
|
+
const { flags, positional } = parseArgs(rest);
|
|
2638
|
+
const config = loadConfig();
|
|
2639
|
+
const profileName = flags.profile || config.currentProfile;
|
|
2640
|
+
const target = getWorkspaceTarget(config, profileName, flags);
|
|
2641
|
+
|
|
2642
|
+
if (subcommand === 'releases' || subcommand === 'list') {
|
|
2643
|
+
const data = await requestWithAuth(
|
|
2644
|
+
config,
|
|
2645
|
+
target.profileName,
|
|
2646
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/runtime/releases`
|
|
2647
|
+
);
|
|
2648
|
+
if (flags.json) return writeJson(data);
|
|
2649
|
+
print(JSON.stringify(data, null, 2));
|
|
2650
|
+
return;
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
if (subcommand === 'activate') {
|
|
2654
|
+
const releaseId = positional[0] || flags['release-id'];
|
|
2655
|
+
if (!releaseId) fail('用法: openxiangda runtime activate <releaseId> [--profile name]');
|
|
2656
|
+
const data = await requestWithAuth(
|
|
2657
|
+
config,
|
|
2658
|
+
target.profileName,
|
|
2659
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/runtime/releases/${encodeURIComponent(releaseId)}/activate`,
|
|
2660
|
+
{ method: 'POST' }
|
|
2661
|
+
);
|
|
2662
|
+
saveRuntimeReleaseState(target, data);
|
|
2663
|
+
if (flags.json) return writeJson(data);
|
|
2664
|
+
print(`runtime release 已激活: ${data.id || releaseId} build=${data.buildId || '-'}`);
|
|
2665
|
+
return;
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
if (subcommand === 'deploy') {
|
|
2669
|
+
const buildId = normalizeRuntimeBuildId(flags['build-id'] || createRuntimeBuildId());
|
|
2670
|
+
const distDir = path.resolve(process.cwd(), flags.dist || 'dist');
|
|
2671
|
+
const assetBaseUrl = buildRuntimeAssetBaseUrl(target.appType, buildId);
|
|
2672
|
+
if (!flags['no-build']) {
|
|
2673
|
+
runRuntimeBuild({
|
|
2674
|
+
buildId,
|
|
2675
|
+
appType: target.appType,
|
|
2676
|
+
assetBaseUrl,
|
|
2677
|
+
command: flags['build-command'],
|
|
2678
|
+
});
|
|
2679
|
+
}
|
|
2680
|
+
const files = collectRuntimeDistFiles(distDir, {
|
|
2681
|
+
includeSourceMaps: Boolean(flags['include-sourcemaps']),
|
|
2682
|
+
});
|
|
2683
|
+
const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
|
|
2684
|
+
const data = await requestWithAuth(
|
|
2685
|
+
config,
|
|
2686
|
+
target.profileName,
|
|
2687
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/runtime/releases`,
|
|
2688
|
+
{
|
|
2689
|
+
method: 'POST',
|
|
2690
|
+
body: {
|
|
2691
|
+
buildId,
|
|
2692
|
+
version: flags.version || readWorkspaceVersion(),
|
|
2693
|
+
releaseNotes: flags.notes || flags['release-notes'] || '',
|
|
2694
|
+
activate: !flags['no-activate'],
|
|
2695
|
+
files,
|
|
2696
|
+
},
|
|
2697
|
+
}
|
|
2698
|
+
);
|
|
2699
|
+
saveRuntimeReleaseState(target, data);
|
|
2700
|
+
const result = {
|
|
2701
|
+
appType: target.appType,
|
|
2702
|
+
buildId,
|
|
2703
|
+
release: data,
|
|
2704
|
+
distDir,
|
|
2705
|
+
fileCount: files.length,
|
|
2706
|
+
totalBytes,
|
|
2707
|
+
assetBaseUrl,
|
|
2708
|
+
activated: !flags['no-activate'],
|
|
2709
|
+
};
|
|
2710
|
+
if (flags.json) return writeJson(result);
|
|
2711
|
+
print(
|
|
2712
|
+
[
|
|
2713
|
+
`runtime release 已上传: ${target.appType}`,
|
|
2714
|
+
`build: ${buildId}`,
|
|
2715
|
+
`files: ${files.length} (${formatBytes(totalBytes)})`,
|
|
2716
|
+
`assetBase: ${assetBaseUrl}`,
|
|
2717
|
+
`active: ${result.activated ? 'yes' : 'no'}`,
|
|
2718
|
+
].join('\n')
|
|
2719
|
+
);
|
|
2720
|
+
return;
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
fail('用法: openxiangda runtime deploy|releases|activate [--profile name]');
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
function runRuntimeBuild(options) {
|
|
2727
|
+
const command = options.command || defaultRuntimeBuildCommand();
|
|
2728
|
+
print(`构建 React SPA runtime: ${command}`);
|
|
2729
|
+
const result = spawnSync(command, [], {
|
|
2730
|
+
cwd: process.cwd(),
|
|
2731
|
+
shell: true,
|
|
2732
|
+
stdio: 'inherit',
|
|
2733
|
+
env: {
|
|
2734
|
+
...process.env,
|
|
2735
|
+
OPENXIANGDA_APP_TYPE: options.appType,
|
|
2736
|
+
APP_TYPE: options.appType,
|
|
2737
|
+
OPENXIANGDA_BUILD_ID: options.buildId,
|
|
2738
|
+
OPENXIANGDA_RUNTIME_ASSET_BASE: options.assetBaseUrl,
|
|
2739
|
+
},
|
|
2740
|
+
});
|
|
2741
|
+
if (result.error) fail(`runtime build 无法启动: ${result.error.message}`);
|
|
2742
|
+
if (result.status !== 0) fail(`runtime build 失败: exit ${result.status}`);
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
function defaultRuntimeBuildCommand() {
|
|
2746
|
+
if (fs.existsSync(path.join(process.cwd(), 'pnpm-lock.yaml'))) return 'pnpm build';
|
|
2747
|
+
if (fs.existsSync(path.join(process.cwd(), 'yarn.lock'))) return 'yarn build';
|
|
2748
|
+
return 'npm run build';
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
function collectRuntimeDistFiles(distDir, options = {}) {
|
|
2752
|
+
if (!fs.existsSync(distDir) || !fs.statSync(distDir).isDirectory()) {
|
|
2753
|
+
fail(`runtime dist 目录不存在: ${distDir}`);
|
|
2754
|
+
}
|
|
2755
|
+
const files = [];
|
|
2756
|
+
walkRuntimeDist(distDir, distDir, files, options);
|
|
2757
|
+
if (!files.some(file => file.path === 'index.html')) {
|
|
2758
|
+
fail(`runtime dist 缺少 index.html: ${distDir}`);
|
|
2759
|
+
}
|
|
2760
|
+
const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
|
|
2761
|
+
if (totalBytes > 25 * 1024 * 1024) {
|
|
2762
|
+
fail(`runtime dist 过大: ${formatBytes(totalBytes)},当前 JSON 发布通道上限为 25MB`);
|
|
2763
|
+
}
|
|
2764
|
+
return files;
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
function walkRuntimeDist(rootDir, currentDir, files, options) {
|
|
2768
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
2769
|
+
for (const entry of entries) {
|
|
2770
|
+
const absolute = path.join(currentDir, entry.name);
|
|
2771
|
+
if (entry.isDirectory()) {
|
|
2772
|
+
walkRuntimeDist(rootDir, absolute, files, options);
|
|
2773
|
+
continue;
|
|
2774
|
+
}
|
|
2775
|
+
if (!entry.isFile()) continue;
|
|
2776
|
+
if (!options.includeSourceMaps && entry.name.endsWith('.map')) continue;
|
|
2777
|
+
const relative = path.relative(rootDir, absolute).replace(/\\/g, '/');
|
|
2778
|
+
const buffer = fs.readFileSync(absolute);
|
|
2779
|
+
files.push({
|
|
2780
|
+
path: relative,
|
|
2781
|
+
size: buffer.length,
|
|
2782
|
+
sha256: crypto.createHash('sha256').update(buffer).digest('hex'),
|
|
2783
|
+
contentType: inferRuntimeContentType(relative),
|
|
2784
|
+
contentBase64: buffer.toString('base64'),
|
|
2785
|
+
});
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2789
|
+
function buildRuntimeAssetBaseUrl(appType, buildId) {
|
|
2790
|
+
return `/service/openxiangda-api/v1/apps/${encodeURIComponent(appType)}/runtime/releases/by-build/${encodeURIComponent(buildId)}/files/`;
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
function createRuntimeBuildId() {
|
|
2794
|
+
const stamp = new Date()
|
|
2795
|
+
.toISOString()
|
|
2796
|
+
.replace(/[-:.TZ]/g, '')
|
|
2797
|
+
.slice(0, 14);
|
|
2798
|
+
return `${stamp}-${crypto.randomBytes(4).toString('hex')}`;
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
function normalizeRuntimeBuildId(value) {
|
|
2802
|
+
const normalized = String(value || '').trim().replace(/[^a-zA-Z0-9_.:-]/g, '-');
|
|
2803
|
+
if (!normalized) fail('buildId 不能为空');
|
|
2804
|
+
return normalized.slice(0, 128);
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
function inferRuntimeContentType(filePath) {
|
|
2808
|
+
const extension = String(filePath || '').split('.').pop().toLowerCase();
|
|
2809
|
+
if (extension === 'html') return 'text/html; charset=utf-8';
|
|
2810
|
+
if (extension === 'js' || extension === 'mjs') return 'application/javascript; charset=utf-8';
|
|
2811
|
+
if (extension === 'css') return 'text/css; charset=utf-8';
|
|
2812
|
+
if (extension === 'json') return 'application/json; charset=utf-8';
|
|
2813
|
+
if (extension === 'svg') return 'image/svg+xml';
|
|
2814
|
+
if (extension === 'png') return 'image/png';
|
|
2815
|
+
if (extension === 'jpg' || extension === 'jpeg') return 'image/jpeg';
|
|
2816
|
+
if (extension === 'webp') return 'image/webp';
|
|
2817
|
+
if (extension === 'woff') return 'font/woff';
|
|
2818
|
+
if (extension === 'woff2') return 'font/woff2';
|
|
2819
|
+
return 'application/octet-stream';
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
function readWorkspaceVersion() {
|
|
2823
|
+
const packageFile = path.join(process.cwd(), 'package.json');
|
|
2824
|
+
if (!fs.existsSync(packageFile)) return '';
|
|
2825
|
+
try {
|
|
2826
|
+
const pkg = JSON.parse(fs.readFileSync(packageFile, 'utf8'));
|
|
2827
|
+
return String(pkg.version || '');
|
|
2828
|
+
} catch {
|
|
2829
|
+
return '';
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
function saveRuntimeReleaseState(target, release) {
|
|
2834
|
+
target.bound.runtime = {
|
|
2835
|
+
...(target.bound.runtime || {}),
|
|
2836
|
+
activeReleaseId: release?.id || target.bound.runtime?.activeReleaseId || null,
|
|
2837
|
+
activeBuildId: release?.buildId || target.bound.runtime?.activeBuildId || null,
|
|
2838
|
+
assetBaseUrl: release?.assetBaseUrl || target.bound.runtime?.assetBaseUrl || null,
|
|
2839
|
+
updatedAt: new Date().toISOString(),
|
|
2840
|
+
};
|
|
2841
|
+
target.bound.updatedAt = new Date().toISOString();
|
|
2842
|
+
if (target.state.profiles?.[target.profileName]) {
|
|
2843
|
+
target.state.profiles[target.profileName] = target.bound;
|
|
2844
|
+
}
|
|
2845
|
+
saveProjectState(target.state);
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
function formatBytes(value) {
|
|
2849
|
+
const bytes = Number(value || 0);
|
|
2850
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
2851
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
2852
|
+
return `${(bytes / 1024 / 1024).toFixed(2)}MB`;
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2608
2855
|
async function inspect(args) {
|
|
2609
2856
|
const [subcommand, ...rest] = args;
|
|
2610
2857
|
const { flags, positional } = parseArgs(rest);
|
|
@@ -2705,7 +2952,7 @@ async function commands(args) {
|
|
|
2705
2952
|
'permission page-group-list|page-group-create|page-group-bind',
|
|
2706
2953
|
'permission form-group-list|form-group-create|form-group-bind|form-summary|menu-permissions',
|
|
2707
2954
|
'settings get|save|indexes|indexes-save|data-management|data-management-save|public-access|public-access-save|public-access-delete',
|
|
2708
|
-
'resource validate|plan|publish|pull',
|
|
2955
|
+
'resource validate|plan|publish|pull|typegen',
|
|
2709
2956
|
'inspect app|form|workflow|automation|permissions',
|
|
2710
2957
|
'feedback preview|submit',
|
|
2711
2958
|
'skill install|status|bootstrap',
|
|
@@ -3055,6 +3302,8 @@ function saveMenuResource(target, menuCode, menuId, extra = {}) {
|
|
|
3055
3302
|
'routeKey',
|
|
3056
3303
|
'legacyFormUuid',
|
|
3057
3304
|
'parentId',
|
|
3305
|
+
'routeCode',
|
|
3306
|
+
'path',
|
|
3058
3307
|
];
|
|
3059
3308
|
saveStateResource(target, 'menus', menuCode, {
|
|
3060
3309
|
...pickStateFields(extra, keys),
|
|
@@ -3359,6 +3608,57 @@ function validateWorkspaceResources(manifest) {
|
|
|
3359
3608
|
};
|
|
3360
3609
|
}
|
|
3361
3610
|
|
|
3611
|
+
function generateResourceTypes(manifest, outputFile) {
|
|
3612
|
+
const filePath = path.resolve(outputFile || path.join('src', 'generated', 'openxiangda.gen.ts'));
|
|
3613
|
+
const menus = (manifest.menus || []).map(item => ({
|
|
3614
|
+
code: item.code,
|
|
3615
|
+
routeCode: item.routeCode || null,
|
|
3616
|
+
path: item.path || null,
|
|
3617
|
+
}));
|
|
3618
|
+
const pagePermissionGroups = (manifest.pagePermissionGroups || []).map(item => ({
|
|
3619
|
+
code: item.code,
|
|
3620
|
+
menuCodes: normalizePermissionCodeArray(item.menuCodes),
|
|
3621
|
+
routeCodes: normalizePermissionCodeArray(item.routeCodes),
|
|
3622
|
+
pathPatterns: normalizePermissionCodeArray(item.pathPatterns),
|
|
3623
|
+
}));
|
|
3624
|
+
const routeCodes = unique(
|
|
3625
|
+
[
|
|
3626
|
+
...menus.map(item => item.routeCode),
|
|
3627
|
+
...pagePermissionGroups.flatMap(item => item.routeCodes),
|
|
3628
|
+
].filter(Boolean)
|
|
3629
|
+
);
|
|
3630
|
+
const menuCodes = unique(menus.map(item => item.code).filter(Boolean));
|
|
3631
|
+
const pagePermissionGroupCodes = unique(
|
|
3632
|
+
pagePermissionGroups.map(item => item.code).filter(Boolean)
|
|
3633
|
+
);
|
|
3634
|
+
const content = [
|
|
3635
|
+
'/* eslint-disable */',
|
|
3636
|
+
'// Generated by openxiangda resource typegen. Do not edit manually.',
|
|
3637
|
+
'',
|
|
3638
|
+
`export const menuCodes = ${JSON.stringify(menuCodes, null, 2)} as const`,
|
|
3639
|
+
'export type MenuCode = typeof menuCodes[number]',
|
|
3640
|
+
'',
|
|
3641
|
+
`export const routeCodes = ${JSON.stringify(routeCodes, null, 2)} as const`,
|
|
3642
|
+
'export type RouteCode = typeof routeCodes[number]',
|
|
3643
|
+
'',
|
|
3644
|
+
`export const pagePermissionGroupCodes = ${JSON.stringify(pagePermissionGroupCodes, null, 2)} as const`,
|
|
3645
|
+
'export type PagePermissionGroupCode = typeof pagePermissionGroupCodes[number]',
|
|
3646
|
+
'',
|
|
3647
|
+
`export const runtimeMenus = ${JSON.stringify(menus, null, 2)} as const`,
|
|
3648
|
+
'',
|
|
3649
|
+
`export const pagePermissionGroups = ${JSON.stringify(pagePermissionGroups, null, 2)} as const`,
|
|
3650
|
+
'',
|
|
3651
|
+
].join('\n');
|
|
3652
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
3653
|
+
fs.writeFileSync(filePath, content);
|
|
3654
|
+
return {
|
|
3655
|
+
filePath,
|
|
3656
|
+
menuCodes: menuCodes.length,
|
|
3657
|
+
routeCodes: routeCodes.length,
|
|
3658
|
+
pagePermissionGroups: pagePermissionGroupCodes.length,
|
|
3659
|
+
};
|
|
3660
|
+
}
|
|
3661
|
+
|
|
3362
3662
|
function validateResourceItem(kind, item, errors, warnings) {
|
|
3363
3663
|
const label = resourceLabel(kind, item);
|
|
3364
3664
|
if (item.__invalid) {
|
|
@@ -4259,6 +4559,8 @@ async function publishMenuResources(config, target, menus, result) {
|
|
|
4259
4559
|
sortOrder: menuItem.sortOrder,
|
|
4260
4560
|
icon: menuItem.icon || null,
|
|
4261
4561
|
isHidden: menuItem.isHidden,
|
|
4562
|
+
routeCode: menuItem.routeCode || null,
|
|
4563
|
+
path: menuItem.path || null,
|
|
4262
4564
|
};
|
|
4263
4565
|
const data = existing
|
|
4264
4566
|
? await requestWithAuth(
|
|
@@ -4281,6 +4583,10 @@ async function publishMenuResources(config, target, menus, result) {
|
|
|
4281
4583
|
...(formUuid ? { formUuid } : {}),
|
|
4282
4584
|
...(pageId ? { pageId } : {}),
|
|
4283
4585
|
...(menuItem.pageCode ? { pageCode: menuItem.pageCode } : {}),
|
|
4586
|
+
...(menuItem.routeCode || data.routeCode
|
|
4587
|
+
? { routeCode: data.routeCode || menuItem.routeCode }
|
|
4588
|
+
: {}),
|
|
4589
|
+
...(menuItem.path || data.path ? { path: data.path || menuItem.path } : {}),
|
|
4284
4590
|
});
|
|
4285
4591
|
}
|
|
4286
4592
|
result.published.push({ kind: 'menu', code: menuItem.code, action: existing ? 'update' : 'create', id: data?.id });
|
|
@@ -4615,6 +4921,9 @@ async function publishPagePermissionGroupResources(config, target, groups, resul
|
|
|
4615
4921
|
name: group.name || group.code,
|
|
4616
4922
|
roles: group.roles || [],
|
|
4617
4923
|
menuFormUuids: resolvePagePermissionGroupTargets(target.bound, group),
|
|
4924
|
+
menuCodes: normalizePermissionCodeArray(group.menuCodes),
|
|
4925
|
+
routeCodes: normalizePermissionCodeArray(group.routeCodes),
|
|
4926
|
+
pathPatterns: normalizePermissionCodeArray(group.pathPatterns),
|
|
4618
4927
|
};
|
|
4619
4928
|
const data = existing
|
|
4620
4929
|
? await requestWithAuth(
|
|
@@ -5174,6 +5483,9 @@ async function pullResources(config, target) {
|
|
|
5174
5483
|
name: group.name,
|
|
5175
5484
|
roles: group.roles || [],
|
|
5176
5485
|
...targets,
|
|
5486
|
+
...arrayField('menuCodes', group.menuCodes),
|
|
5487
|
+
...arrayField('routeCodes', group.routeCodes),
|
|
5488
|
+
...arrayField('pathPatterns', group.pathPatterns),
|
|
5177
5489
|
});
|
|
5178
5490
|
written.push(path.relative(process.cwd(), filePath));
|
|
5179
5491
|
}
|
|
@@ -5288,6 +5600,11 @@ function splitPagePermissionTargetsForManifest(values = [], lookups) {
|
|
|
5288
5600
|
};
|
|
5289
5601
|
}
|
|
5290
5602
|
|
|
5603
|
+
function arrayField(key, value) {
|
|
5604
|
+
const items = normalizePermissionCodeArray(value);
|
|
5605
|
+
return items.length > 0 ? { [key]: items } : {};
|
|
5606
|
+
}
|
|
5607
|
+
|
|
5291
5608
|
function toPulledNotificationTemplate(template, lookups) {
|
|
5292
5609
|
const formCode = lookups.formCodeByUuid.get(template.formUuid);
|
|
5293
5610
|
return stripUndefinedValues({
|
|
@@ -5666,6 +5983,14 @@ function resolvePagePermissionGroupTargets(bound, group) {
|
|
|
5666
5983
|
return unique([...direct, ...fromMenus, ...fromForms, ...fromPages].filter(Boolean));
|
|
5667
5984
|
}
|
|
5668
5985
|
|
|
5986
|
+
function normalizePermissionCodeArray(value) {
|
|
5987
|
+
return unique(
|
|
5988
|
+
(Array.isArray(value) ? value : [])
|
|
5989
|
+
.map(item => String(item || '').trim())
|
|
5990
|
+
.filter(Boolean)
|
|
5991
|
+
);
|
|
5992
|
+
}
|
|
5993
|
+
|
|
5669
5994
|
function saveResourceEntry(target, bucket, code, extra = {}) {
|
|
5670
5995
|
saveStateResource(target, bucket, code, pickStateFields(extra, ['formUuid']), [
|
|
5671
5996
|
'formUuid',
|
|
@@ -5702,7 +6027,9 @@ function menuEquals(bound, desired, existing) {
|
|
|
5702
6027
|
optionalScalarEquals(existing.parentId || null, desiredParentId, hasAnyKey(desired, ['parentCode', 'parentId'])) &&
|
|
5703
6028
|
optionalScalarEquals(existing.sortOrder, desired.sortOrder, desired.sortOrder !== undefined) &&
|
|
5704
6029
|
optionalScalarEquals(existing.icon || null, desired.icon || null, desired.icon !== undefined) &&
|
|
5705
|
-
optionalScalarEquals(Boolean(existing.isHidden), Boolean(desired.isHidden), desired.isHidden !== undefined)
|
|
6030
|
+
optionalScalarEquals(Boolean(existing.isHidden), Boolean(desired.isHidden), desired.isHidden !== undefined) &&
|
|
6031
|
+
optionalScalarEquals(existing.routeCode || null, desired.routeCode || null, desired.routeCode !== undefined) &&
|
|
6032
|
+
optionalScalarEquals(existing.path || null, desired.path || null, desired.path !== undefined)
|
|
5706
6033
|
);
|
|
5707
6034
|
}
|
|
5708
6035
|
|
|
@@ -5754,7 +6081,10 @@ function pagePermissionGroupEquals(bound, desired, existing) {
|
|
|
5754
6081
|
String(existing.name || '') === String(desired.name || desired.code || '') &&
|
|
5755
6082
|
String(existing.resourceCode || '') === String(desired.code || '') &&
|
|
5756
6083
|
stringSetEquals(existing.roles || [], desired.roles || []) &&
|
|
5757
|
-
stringSetEquals(existing.menuFormUuids || [], desiredTargets)
|
|
6084
|
+
stringSetEquals(existing.menuFormUuids || [], desiredTargets) &&
|
|
6085
|
+
stringSetEquals(existing.menuCodes || [], normalizePermissionCodeArray(desired.menuCodes)) &&
|
|
6086
|
+
stringSetEquals(existing.routeCodes || [], normalizePermissionCodeArray(desired.routeCodes)) &&
|
|
6087
|
+
stringSetEquals(existing.pathPatterns || [], normalizePermissionCodeArray(desired.pathPatterns))
|
|
5758
6088
|
);
|
|
5759
6089
|
}
|
|
5760
6090
|
|
package/lib/workspace-init.js
CHANGED
|
@@ -4,7 +4,8 @@ const { spawnSync } = require('child_process');
|
|
|
4
4
|
const { getProfile, loadConfig, saveProjectState } = require('./config');
|
|
5
5
|
|
|
6
6
|
const ROOT_DIR = path.join(__dirname, '..');
|
|
7
|
-
const
|
|
7
|
+
const LEGACY_TEMPLATE_DIR = path.join(ROOT_DIR, 'templates', 'sy-lowcode-app-workspace');
|
|
8
|
+
const REACT_SPA_TEMPLATE_DIR = path.join(ROOT_DIR, 'templates', 'openxiangda-react-spa');
|
|
8
9
|
|
|
9
10
|
function initWorkspace(options = {}) {
|
|
10
11
|
const targetDir = path.resolve(options.dir || process.cwd());
|
|
@@ -14,13 +15,16 @@ function initWorkspace(options = {}) {
|
|
|
14
15
|
const install = Boolean(options.install);
|
|
15
16
|
const profileName = options.profile || null;
|
|
16
17
|
const appType = options.appType || null;
|
|
18
|
+
const runtime = normalizeRuntime(options.runtime || options.template);
|
|
19
|
+
const templateDir =
|
|
20
|
+
runtime === 'react-spa' ? REACT_SPA_TEMPLATE_DIR : LEGACY_TEMPLATE_DIR;
|
|
17
21
|
|
|
18
22
|
if ((profileName && !appType) || (!profileName && appType)) {
|
|
19
23
|
throw new Error('workspace init 绑定应用时必须同时提供 --profile 和 --app-type');
|
|
20
24
|
}
|
|
21
25
|
|
|
22
|
-
ensureCanInitialize(targetDir, force);
|
|
23
|
-
copyTemplate(
|
|
26
|
+
ensureCanInitialize(targetDir, force, templateDir);
|
|
27
|
+
copyTemplate(templateDir, targetDir, {
|
|
24
28
|
__WORKSPACE_PACKAGE_NAME__: packageName,
|
|
25
29
|
});
|
|
26
30
|
|
|
@@ -69,7 +73,8 @@ function initWorkspace(options = {}) {
|
|
|
69
73
|
return {
|
|
70
74
|
targetDir,
|
|
71
75
|
packageName,
|
|
72
|
-
|
|
76
|
+
runtime,
|
|
77
|
+
templateDir,
|
|
73
78
|
installedDependencies: install,
|
|
74
79
|
bound,
|
|
75
80
|
nextSteps: buildNextSteps(targetDir, install, bound),
|
|
@@ -78,13 +83,16 @@ function initWorkspace(options = {}) {
|
|
|
78
83
|
|
|
79
84
|
function assertCanInitializeWorkspace(options = {}) {
|
|
80
85
|
const targetDir = path.resolve(options.dir || process.cwd());
|
|
81
|
-
|
|
86
|
+
const runtime = normalizeRuntime(options.runtime || options.template);
|
|
87
|
+
const templateDir =
|
|
88
|
+
runtime === 'react-spa' ? REACT_SPA_TEMPLATE_DIR : LEGACY_TEMPLATE_DIR;
|
|
89
|
+
ensureCanInitialize(targetDir, Boolean(options.force), templateDir);
|
|
82
90
|
return targetDir;
|
|
83
91
|
}
|
|
84
92
|
|
|
85
|
-
function ensureCanInitialize(targetDir, force) {
|
|
86
|
-
if (!fs.existsSync(
|
|
87
|
-
throw new Error(`workspace 模板不存在: ${
|
|
93
|
+
function ensureCanInitialize(targetDir, force, templateDir) {
|
|
94
|
+
if (!fs.existsSync(templateDir)) {
|
|
95
|
+
throw new Error(`workspace 模板不存在: ${templateDir}`);
|
|
88
96
|
}
|
|
89
97
|
if (!fs.existsSync(targetDir)) {
|
|
90
98
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
@@ -101,6 +109,10 @@ function ensureCanInitialize(targetDir, force) {
|
|
|
101
109
|
}
|
|
102
110
|
}
|
|
103
111
|
|
|
112
|
+
function normalizeRuntime(value) {
|
|
113
|
+
return value === 'react-spa' || value === 'spa' ? 'react-spa' : 'legacy';
|
|
114
|
+
}
|
|
115
|
+
|
|
104
116
|
function copyTemplate(sourceDir, targetDir, replacements) {
|
|
105
117
|
for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
|
|
106
118
|
const sourcePath = path.join(sourceDir, entry.name);
|
|
@@ -7,7 +7,7 @@ description: Use OpenXiangda for ANY work inside a sy-lowcode-app-workspace or a
|
|
|
7
7
|
|
|
8
8
|
OpenXiangda is a lightweight bridge between an AI coding tool and a private low-code platform. No AK/SK. The user provides a platform domain, logs in as a normal platform user, and the CLI calls `/openxiangda-api/v1` with that user's token.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
OpenXiangda supports two workspace modes. Classic `sy-lowcode-app-workspace` publishes form/page bundles with `openxiangda workspace publish`. Phase 6 `react-spa` workspaces are standard React/Vite apps: publish resources with `openxiangda resource publish`, then publish the frontend dist with `openxiangda runtime deploy`. Do not send React SPA routes through old `isRenderNav`, app-shell, or workbench page parameters.
|
|
11
11
|
|
|
12
12
|
## TL;DR — Decision Card
|
|
13
13
|
|
|
@@ -17,7 +17,8 @@ For any user-facing page (form pages, workflow form pages, custom code pages), O
|
|
|
17
17
|
|
|
18
18
|
| User says (zh / en) | Skill | First command |
|
|
19
19
|
|---|---|---|
|
|
20
|
-
| 发布
|
|
20
|
+
| 发布 classic workspace / 上线旧模式 | `openxiangda-core` | `openxiangda workspace publish --profile <name> --changed --dry-run` |
|
|
21
|
+
| 发布 React SPA / Phase 6 runtime | `openxiangda-core` | `openxiangda resource publish --profile <name>` then `openxiangda runtime deploy --profile <name>` |
|
|
21
22
|
| 只发布改动 / 增量 / 单页 / 单表 | `openxiangda-core` | `... --changed` / `--page <code>` / `--form <code>` / `--only pages/a,forms/b` |
|
|
22
23
|
| 创建应用 / 新建 app / scaffold / 初始化工作区 | `openxiangda-app` | `openxiangda workspace init <dir> --profile <name> --app-name "..."` |
|
|
23
24
|
| 绑定已有应用 / bind existing app | `openxiangda-app` | `openxiangda workspace bind --profile <name> --app-type APP_XXX` |
|
|
@@ -33,7 +34,7 @@ For any user-facing page (form pages, workflow form pages, custom code pages), O
|
|
|
33
34
|
|
|
34
35
|
### Hard rules — always
|
|
35
36
|
|
|
36
|
-
- ✅ Publish through `openxiangda workspace publish --profile <name
|
|
37
|
+
- ✅ Publish classic workspaces through `openxiangda workspace publish --profile <name>`; publish React SPA workspaces through `openxiangda resource publish` + `openxiangda runtime deploy`.
|
|
37
38
|
- ✅ User token lives in `~/.openxiangda/profiles.json`; project state in `.openxiangda/state.json` (IDs only).
|
|
38
39
|
- ✅ Each profile (dev / prod / ...) has its own `appType` and resource IDs; never copy a `formUuid` / `pageId` / `workflowId` / `automationId` across profiles.
|
|
39
40
|
- ✅ Run `openxiangda update check --json` at the start of substantial work; if `updateAvailable`, run `openxiangda update install` and `openxiangda skill install --force`.
|
|
@@ -40,6 +40,32 @@ cd ./my-app-workspace
|
|
|
40
40
|
pnpm install
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
For new Phase 6 React SPA apps, use the standard React application template:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
openxiangda workspace init ./my-react-app \
|
|
47
|
+
--profile <name> \
|
|
48
|
+
--app-name "应用名称" \
|
|
49
|
+
--runtime react-spa
|
|
50
|
+
cd ./my-react-app
|
|
51
|
+
pnpm install
|
|
52
|
+
pnpm dev
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
React SPA workspaces use React Router, Tailwind CSS, `OpenXiangdaProvider`
|
|
56
|
+
from `openxiangda/runtime/react`, and resources under `src/resources/`.
|
|
57
|
+
Do not use old `isRenderNav`, app-shell, or workbench page parameters inside
|
|
58
|
+
React SPA routes.
|
|
59
|
+
|
|
60
|
+
Publish React SPA apps with:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
openxiangda resource publish --profile <name>
|
|
64
|
+
openxiangda runtime deploy --profile <name>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`runtime deploy` builds and activates the app-level SPA release for `/view/:appType/*`.
|
|
68
|
+
|
|
43
69
|
Bind to an existing app only when the user explicitly provides `appType` or asks to reuse an existing platform app:
|
|
44
70
|
|
|
45
71
|
```bash
|
|
@@ -59,6 +85,7 @@ If the user explicitly asks to create an app without initializing a workspace:
|
|
|
59
85
|
|
|
60
86
|
```bash
|
|
61
87
|
openxiangda app create "应用名称" --profile <name>
|
|
88
|
+
openxiangda app create "React应用名称" --profile <name> --runtime react-spa
|
|
62
89
|
openxiangda workspace bind --profile <name> --app-type APP_XXX
|
|
63
90
|
```
|
|
64
91
|
|
|
@@ -71,6 +98,7 @@ openxiangda workspace bind --profile <name> --app-type APP_XXX
|
|
|
71
98
|
- Store platform-specific IDs only in `.openxiangda/state.json`.
|
|
72
99
|
- Use logical local codes for forms, pages, workflows, automations, and menus.
|
|
73
100
|
- Before scaffolding a new business app, read `../../references/best-practices.md` and pick an architecture from the initialized `examples/best-practices/` catalog. Do not generate a large single-file app page when a template pattern already covers the scenario.
|
|
101
|
+
- For Phase 6 React SPA apps, use `--runtime react-spa`. The generated app owns routing/layouts/menus with React Router and Tailwind CSS; platform permissions still come from backend runtime bootstrap and route checks.
|
|
74
102
|
- When publishing after app edits, prefer targeted commands (`workspace publish --changed --dry-run`, then `--changed`, `--page`, `--form`, or `--only`). Do not publish all forms/pages just because one page changed.
|
|
75
103
|
- Use `openxiangda app snapshot <APP_XXX> --profile <name> --json` before changing an existing app.
|
|
76
104
|
|