45eec17007
新增 HostBridge appearance.getColorScheme 只读契约和 H5 facade Expo 壳通过 React Native Appearance 读取系统配色 Tauri 壳通过主窗口 theme 读取桌面配色 补齐外观查询测试、漂移检查和架构文档
180 lines
5.3 KiB
JavaScript
180 lines
5.3 KiB
JavaScript
import fs from 'node:fs';
|
|
|
|
const configPath = new URL('../src-tauri/tauri.conf.json', import.meta.url);
|
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
const capabilityPath = new URL(
|
|
'../src-tauri/capabilities/main.json',
|
|
import.meta.url,
|
|
);
|
|
const capability = JSON.parse(fs.readFileSync(capabilityPath, 'utf8'));
|
|
const buildScriptPath = new URL('../src-tauri/build.rs', import.meta.url);
|
|
const buildScript = fs.readFileSync(buildScriptPath, 'utf8');
|
|
const iconPath = new URL('../src-tauri/icons/icon.png', import.meta.url);
|
|
const sharedContractPath = new URL(
|
|
'../../../packages/shared/src/contracts/hostBridge.ts',
|
|
import.meta.url,
|
|
);
|
|
const sharedContractSource = fs.readFileSync(sharedContractPath, 'utf8');
|
|
const mainPath = new URL('../src-tauri/src/main.rs', import.meta.url);
|
|
const main = fs.readFileSync(mainPath, 'utf8');
|
|
|
|
function extractStringArrayExport(source, exportName) {
|
|
const match = source.match(
|
|
new RegExp(`export const ${exportName}[^=]*= \\[([\\s\\S]*?)\\](?: as const)?;`),
|
|
);
|
|
if (!match) {
|
|
throw new Error(`unable to read ${exportName}`);
|
|
}
|
|
|
|
const entries = [...match[1].matchAll(/'([^']+)'/g)].map(
|
|
(entry) => entry[1],
|
|
);
|
|
if (match[1].includes('...HOST_BRIDGE_METHODS')) {
|
|
return [
|
|
...extractStringArrayExport(source, 'HOST_BRIDGE_METHODS'),
|
|
...entries,
|
|
];
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
function extractDesktopCapabilities(source) {
|
|
const match = source.match(/fn capabilities\(\)[^{]*\{[\s\S]*?vec!\[([\s\S]*?)\]\s*\}/);
|
|
if (!match) {
|
|
throw new Error('unable to read desktop shell capabilities');
|
|
}
|
|
|
|
return [...match[1].matchAll(/"([^"]+)"/g)].map((entry) => entry[1]);
|
|
}
|
|
|
|
function resolveHostCapabilitiesFromUrl(rawUrl) {
|
|
const url = new URL(rawUrl, 'https://app.genarrative.world/');
|
|
return (url.searchParams.get('hostCapabilities') ?? '')
|
|
.split(',')
|
|
.map((capability) => capability.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function assertSameList(actual, expected, label) {
|
|
if (
|
|
actual.length !== expected.length ||
|
|
actual.some((value, index) => value !== expected[index])
|
|
) {
|
|
throw new Error(
|
|
`${label} drifted: expected ${expected.join(', ')} but got ${actual.join(', ')}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
const sharedCapabilities = extractStringArrayExport(
|
|
sharedContractSource,
|
|
'HOST_BRIDGE_CAPABILITIES',
|
|
);
|
|
const desktopCapabilities = extractDesktopCapabilities(main);
|
|
const unknownDesktopCapabilities = desktopCapabilities.filter(
|
|
(capability) => !sharedCapabilities.includes(capability),
|
|
);
|
|
if (unknownDesktopCapabilities.length > 0) {
|
|
throw new Error(
|
|
`desktop shell declares unknown HostBridge capabilities: ${unknownDesktopCapabilities.join(', ')}`,
|
|
);
|
|
}
|
|
|
|
if (config.build?.frontendDist !== '../../../dist') {
|
|
throw new Error('desktop shell must package the root H5 dist');
|
|
}
|
|
|
|
if (config.build?.beforeBuildCommand !== 'npm --prefix ../.. run build:raw && npm run typecheck') {
|
|
throw new Error('desktop shell build command must run from apps/desktop-shell');
|
|
}
|
|
|
|
const [mainWindow] = config.app?.windows ?? [];
|
|
if (!mainWindow || mainWindow.create !== false) {
|
|
throw new Error('desktop shell must create the main window from Rust setup');
|
|
}
|
|
|
|
if (!config.bundle?.icon?.includes('icons/icon.png')) {
|
|
throw new Error('desktop shell must use the real brand icon asset');
|
|
}
|
|
|
|
const requiredUrlParts = [
|
|
'clientRuntime=native_app',
|
|
'clientType=native_app',
|
|
'hostShell=tauri_desktop',
|
|
'hostPlatform=unknown',
|
|
'bridgeVersion=1',
|
|
];
|
|
|
|
for (const part of requiredUrlParts) {
|
|
if (!String(mainWindow.url ?? '').includes(part)) {
|
|
throw new Error(`desktop shell main window URL missing ${part}`);
|
|
}
|
|
if (!String(config.build?.devUrl ?? '').includes(part)) {
|
|
throw new Error(`desktop shell dev URL missing ${part}`);
|
|
}
|
|
}
|
|
|
|
assertSameList(
|
|
resolveHostCapabilitiesFromUrl(mainWindow.url),
|
|
desktopCapabilities,
|
|
'desktop shell main window hostCapabilities',
|
|
);
|
|
assertSameList(
|
|
resolveHostCapabilitiesFromUrl(config.build?.devUrl ?? ''),
|
|
desktopCapabilities,
|
|
'desktop shell dev hostCapabilities',
|
|
);
|
|
|
|
const requiredPermissions = [
|
|
'core:default',
|
|
'allow-host-bridge-request',
|
|
];
|
|
const requiredBuildCommands = ['host_bridge_request'];
|
|
const requiredMainSnippets = [
|
|
'tauri_plugin_clipboard_manager::init()',
|
|
'"appearance.getColorScheme"',
|
|
'"share.open"',
|
|
'"share.setTarget"',
|
|
'"navigation.openNativePage"',
|
|
'"app.setTitle"',
|
|
'"app.setBadgeCount"',
|
|
'"clipboard.writeText"',
|
|
'"file.exportText"',
|
|
'"file.exportImage"',
|
|
'tauri_plugin_dialog::init()',
|
|
'"copied_to_clipboard"',
|
|
'"file export cancelled"',
|
|
'BASE64_STANDARD.decode',
|
|
'set_title',
|
|
'set_badge_count',
|
|
'window.theme()',
|
|
];
|
|
|
|
for (const permission of requiredPermissions) {
|
|
if (!capability.permissions?.includes(permission)) {
|
|
throw new Error(`desktop shell capability missing ${permission}`);
|
|
}
|
|
}
|
|
|
|
for (const command of requiredBuildCommands) {
|
|
if (!buildScript.includes(command)) {
|
|
throw new Error(`desktop shell build manifest missing ${command}`);
|
|
}
|
|
}
|
|
|
|
if (buildScript.includes('resolve_desktop_shell_runtime')) {
|
|
throw new Error('desktop shell build manifest exposes an unused runtime command');
|
|
}
|
|
|
|
const icon = fs.readFileSync(iconPath);
|
|
if (icon.length < 10000) {
|
|
throw new Error('desktop shell icon must use a real brand asset');
|
|
}
|
|
|
|
for (const snippet of requiredMainSnippets) {
|
|
if (!main.includes(snippet)) {
|
|
throw new Error(`desktop shell Rust host bridge missing ${snippet}`);
|
|
}
|
|
}
|