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 packagePath = new URL('../package.json', import.meta.url); const packageConfig = JSON.parse(fs.readFileSync(packagePath, 'utf8')); const rootPackagePath = new URL('../../../package.json', import.meta.url); const rootPackageConfig = JSON.parse(fs.readFileSync(rootPackagePath, '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 cargoManifestPath = new URL('../src-tauri/Cargo.toml', import.meta.url); const cargoManifest = fs.readFileSync(cargoManifestPath, 'utf8'); const iconDirPath = new URL('../src-tauri/icons/', import.meta.url); const generatedPermissionDir = new URL( '../src-tauri/permissions/autogenerated/', 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'); const productionSourceRoots = [ new URL('../package.json', import.meta.url), new URL('../src-tauri/Cargo.toml', import.meta.url), new URL('../src-tauri/build.rs', import.meta.url), new URL('../src-tauri/capabilities/main.json', import.meta.url), new URL('../src-tauri/src/', import.meta.url), new URL('../src-tauri/tauri.conf.json', import.meta.url), ]; const productionFileExtensions = new Set(['.json', '.mjs', '.rs', '.toml']); const devScaffoldTerms = [ 'mo' + 'ck', 'fa' + 'ke', 'place' + 'holder', 'st' + 'ub', 'TO' + 'DO', 'FIX' + 'ME', '占' + '位', '模' + '拟', '伪' + '造', ]; const blockedDesktopNpmDependencies = [ '@amplitude/analytics-browser', '@bugsnag/js', '@datadog/browser-rum', '@segment/analytics-next', '@sentry/browser', '@sentry/react', '@sentry/tauri', '@sentry/tracing', '@tauri-apps/plugin-log', '@tauri-apps/plugin-updater', 'amplitude-js', 'mixpanel-browser', 'posthog-js', ]; const blockedDesktopCargoDependencies = [ 'bugsnag', 'datadog', 'opentelemetry', 'opentelemetry-otlp', 'posthog-rs', 'sentry', 'sentry-tauri', 'tauri-plugin-log', 'tauri-plugin-updater', 'tracing-opentelemetry', ]; const blockedDesktopSdkSnippets = [ '@sentry/', 'Bugsnag.start', 'Datadog', 'Sentry.init', 'analytics.load', 'amplitude.init', 'datadogRum.init', 'mixpanel.init', 'opentelemetry::', 'posthog.init', 'sentry::init', 'sentry_tauri', 'tauri_plugin_log::', 'tauri_plugin_updater::', 'tracing_opentelemetry', ]; const requiredBundleIcons = [ 'icons/32x32.png', 'icons/128x128.png', 'icons/128x128@2x.png', 'icons/icon.icns', 'icons/icon.ico', 'icons/icon.png', ]; const requiredPngIconSizes = new Map([ ['32x32.png', 32], ['128x128.png', 128], ['128x128@2x.png', 256], ['icon.png', 512], ]); function extractCargoPackageString(source, key) { const match = source.match(new RegExp(`^${key}\\s*=\\s*"([^"]+)"`, 'm')); if (!match) { throw new Error(`unable to read Cargo package ${key}`); } return match[1]; } function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function assertNoBlockedNpmDependencies() { const dependencySections = [ 'dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies', ]; for (const dependency of blockedDesktopNpmDependencies) { for (const section of dependencySections) { if (packageConfig[section]?.[dependency]) { throw new Error( `desktop shell must not depend on ${dependency} before real observability/channel contracts exist`, ); } } } } function collectNpmDependencyNames(packageJson) { const dependencySections = [ 'dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies', ]; return dependencySections.flatMap((section) => Object.keys(packageJson[section] ?? {}), ); } function assertNoTauriGuestNpmDependencies(packageJson, packageLabel) { for (const dependency of collectNpmDependencyNames(packageJson)) { if (dependency === '@tauri-apps/api' || dependency.startsWith('@tauri-apps/plugin-')) { throw new Error( `${packageLabel} must not depend on ${dependency}; desktop H5 must use HostBridge through the injected Tauri global only`, ); } } } function assertNoBlockedCargoDependencies() { for (const dependency of blockedDesktopCargoDependencies) { const dependencyPattern = new RegExp( `^\\s*(?:"${escapeRegExp(dependency)}"|${escapeRegExp(dependency)})\\s*=`, 'm', ); if (dependencyPattern.test(cargoManifest)) { throw new Error( `desktop shell must not depend on ${dependency} before real observability/channel contracts exist`, ); } } } function assertNoBlockedDesktopSdkSnippets() { const sources = [ ['tauri.conf.json', JSON.stringify(config)], ['build.rs', buildScript], ['main.rs', main], ]; for (const [sourceName, source] of sources) { for (const snippet of blockedDesktopSdkSnippets) { if (source.includes(snippet)) { throw new Error( `desktop shell ${sourceName} must not initialize ${snippet} before real observability/channel contracts exist`, ); } } } } function collectProductionSourceFiles(entry) { const stats = fs.statSync(entry); if (stats.isDirectory()) { const directory = entry.href.endsWith('/') ? entry : new URL(`${entry.href}/`); return fs .readdirSync(entry, { withFileTypes: true }) .flatMap((child) => collectProductionSourceFiles( new URL(`${child.name}${child.isDirectory() ? '/' : ''}`, directory), ), ); } const path = entry.pathname; const extension = path.match(/\.[^.]+$/)?.[0] ?? ''; if (!productionFileExtensions.has(extension)) { return []; } if (path.includes('.test.') || path.endsWith('/scripts/check-config.mjs')) { return []; } return [entry]; } function assertNoDevScaffoldTerms(files) { for (const file of files) { const source = fs.readFileSync(file, 'utf8'); const lineStarts = [0]; for (let index = 0; index < source.length; index += 1) { if (source[index] === '\n') { lineStarts.push(index + 1); } } for (const term of devScaffoldTerms) { const matchIndex = source.toLowerCase().indexOf(term.toLowerCase()); if (matchIndex === -1) { continue; } const line = lineStarts.filter((start) => start <= matchIndex).length; throw new Error( `desktop shell production source must not include ${term}: ${file.pathname}:${line}`, ); } } } assertNoDevScaffoldTerms( productionSourceRoots.flatMap((root) => collectProductionSourceFiles(root)), ); assertNoBlockedNpmDependencies(); assertNoTauriGuestNpmDependencies(packageConfig, 'desktop shell package'); assertNoTauriGuestNpmDependencies(rootPackageConfig, 'root H5 package'); assertNoBlockedCargoDependencies(); assertNoBlockedDesktopSdkSnippets(); function readPngSize(file) { const buffer = fs.readFileSync(file); if ( buffer.length < 24 || buffer.toString('hex', 0, 8) !== '89504e470d0a1a0a' ) { throw new Error(`desktop shell icon is not a PNG: ${file.pathname}`); } const width = buffer.readUInt32BE(16); const height = buffer.readUInt32BE(20); const dataStart = buffer.indexOf(Buffer.from('IDAT')); const hasTransparencyChunk = buffer.includes(Buffer.from('tRNS')); const hasAlphaColorType = buffer[25] === 4 || buffer[25] === 6; return { width, height, dataStart, hasTransparency: hasTransparencyChunk || hasAlphaColorType, }; } function assertDesktopIconSet() { const icons = config.bundle?.icon ?? []; if ( icons.length !== requiredBundleIcons.length || requiredBundleIcons.some((icon) => !icons.includes(icon)) ) { throw new Error('desktop shell must bundle the full real desktop icon set'); } for (const [fileName, expectedSize] of requiredPngIconSizes) { const icon = readPngSize(new URL(fileName, iconDirPath)); if (icon.width !== expectedSize || icon.height !== expectedSize) { throw new Error(`desktop shell icon ${fileName} must be ${expectedSize}x${expectedSize}`); } if (!icon.hasTransparency || icon.dataStart === -1) { throw new Error(`desktop shell icon ${fileName} must use a real transparent brand asset`); } } const ico = fs.readFileSync(new URL('icon.ico', iconDirPath)); if ( ico.length < 6 || ico.readUInt16LE(0) !== 0 || ico.readUInt16LE(2) !== 1 || ico.readUInt16LE(4) < 3 ) { throw new Error('desktop shell Windows icon must be a multi-size ICO'); } const icns = fs.readFileSync(new URL('icon.icns', iconDirPath)); if ( icns.length < 16 || icns.toString('ascii', 0, 4) !== 'icns' || icns.readUInt32BE(4) !== icns.length ) { throw new Error('desktop shell macOS icon must be a valid ICNS'); } } function extractStringArrayExport(source, exportName, seen = new Set()) { if (seen.has(exportName)) { throw new Error(`cyclic string array export ${exportName}`); } const match = source.match( new RegExp(`export const ${exportName}[^=]*= \\[([\\s\\S]*?)\\](?: as const)?;`), ); if (!match) { throw new Error(`unable to read ${exportName}`); } const nextSeen = new Set(seen); nextSeen.add(exportName); const entries = []; for (const entry of match[1].matchAll(/\.\.\s*([A-Z0-9_]+)|'([^']+)'/g)) { if (entry[1]) { entries.push(...extractStringArrayExport(source, entry[1], nextSeen)); } else { entries.push(entry[2]); } } return entries; } function extractRustStringArrayConst(source, constName) { const match = source.match( new RegExp(`const ${constName}[^=]*= \\[([\\s\\S]*?)\\];`), ); if (!match) { throw new Error(`unable to read Rust const ${constName}`); } return [...match[1].matchAll(/"([^"]+)"/g)].map((entry) => entry[1]); } 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 extractDesktopHandledMethods(source) { const matchBodies = [...source.matchAll(/match request\.method\.as_str\(\) \{([\s\S]*?)\n \}/g)].map( (match) => match[1], ); if (matchBodies.length === 0) { throw new Error('unable to read desktop shell HostBridge handler methods'); } return [ ...new Set( matchBodies.flatMap((body) => [...body.matchAll(/"([^"]+)"\s*=>/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 resolveHostVersionFromUrl(rawUrl) { const url = new URL(rawUrl, 'https://app.genarrative.world/'); return url.searchParams.get('hostVersion'); } 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(', ')}`, ); } } function extractTauriBuildCommands(source) { const match = source.match(/\.commands\(\s*&\[\s*([^\]]*?)\s*\]\s*\)/); if (!match) { throw new Error('unable to read Tauri build manifest commands'); } return [...match[1].matchAll(/"([^"]+)"/g)].map((entry) => entry[1]); } function extractTauriInvokeCommands(source) { const match = source.match(/tauri::generate_handler!\[\s*([^\]]*?)\s*\]/); if (!match) { throw new Error('unable to read Tauri invoke handler commands'); } return match[1] .split(',') .map((command) => command.trim()) .filter(Boolean); } function assertGeneratedPermissions(commandNames) { if (!fs.existsSync(generatedPermissionDir)) { return; } const expectedPermissionFiles = new Set( commandNames.map((command) => `${command}.toml`), ); for (const entry of fs.readdirSync(generatedPermissionDir, { withFileTypes: true, })) { if (entry.isDirectory()) { throw new Error( `desktop shell generated permissions must not include nested directory ${entry.name}`, ); } if (!expectedPermissionFiles.has(entry.name)) { throw new Error( `desktop shell generated permission exposes an unexpected command: ${entry.name}`, ); } const permissionFile = new URL(entry.name, generatedPermissionDir); const permissionSource = fs.readFileSync(permissionFile, 'utf8'); const commandName = entry.name.replace(/\.toml$/, ''); for (const expectedSnippet of [ `identifier = "allow-${commandName.replaceAll('_', '-')}"`, `commands.allow = ["${commandName}"]`, `commands.deny = ["${commandName}"]`, ]) { if (!permissionSource.includes(expectedSnippet)) { throw new Error( `desktop shell generated permission ${entry.name} drifted from ${commandName}`, ); } } } } function assertOnlyMainCapabilityFile() { const capabilityDir = new URL('../src-tauri/capabilities/', import.meta.url); const files = fs .readdirSync(capabilityDir, { withFileTypes: true }) .filter((entry) => entry.isFile()) .map((entry) => entry.name) .sort(); assertSameList(files, ['main.json'], 'desktop shell capability files'); } const sharedCapabilities = extractStringArrayExport( sharedContractSource, 'HOST_BRIDGE_CAPABILITIES', ); const sharedMethods = extractStringArrayExport( sharedContractSource, 'HOST_BRIDGE_METHODS', ); const desktopMethods = extractRustStringArrayConst(main, 'HOST_BRIDGE_METHODS'); const desktopCapabilities = extractDesktopCapabilities(main); const desktopHandledMethods = extractDesktopHandledMethods(main); const sdkBackedCapabilities = ['auth.requestLogin', 'payment.request']; assertSameList(desktopMethods, sharedMethods, 'desktop shell HostBridge method whitelist'); const unknownHandledDesktopMethods = desktopHandledMethods.filter( (method) => !sharedMethods.includes(method), ); if (unknownHandledDesktopMethods.length > 0) { throw new Error( `desktop shell handles unknown HostBridge methods: ${unknownHandledDesktopMethods.join(', ')}`, ); } const unknownDesktopCapabilities = desktopCapabilities.filter( (capability) => !sharedCapabilities.includes(capability), ); if (unknownDesktopCapabilities.length > 0) { throw new Error( `desktop shell declares unknown HostBridge capabilities: ${unknownDesktopCapabilities.join(', ')}`, ); } for (const capability of sdkBackedCapabilities) { if (desktopCapabilities.includes(capability)) { throw new Error( `desktop shell must not declare ${capability} until a real SDK/channel flow is implemented`, ); } } const missingDesktopMethodHandlers = desktopCapabilities.filter( (capability) => sharedMethods.includes(capability) && !desktopHandledMethods.includes(capability), ); if (missingDesktopMethodHandlers.length > 0) { throw new Error( `desktop shell declares request capabilities without HostBridge handlers: ${missingDesktopMethodHandlers.join(', ')}`, ); } const undeclaredDesktopMethodHandlers = desktopHandledMethods.filter( (method) => !desktopCapabilities.includes(method) && !sdkBackedCapabilities.includes(method), ); if (undeclaredDesktopMethodHandlers.length > 0) { throw new Error( `desktop shell handles unadvertised HostBridge methods: ${undeclaredDesktopMethodHandlers.join(', ')}`, ); } if (config.productName !== 'Genarrative') { throw new Error('desktop shell productName must be Genarrative'); } if (config.identifier !== 'world.genarrative.desktop') { throw new Error('desktop shell identifier must be world.genarrative.desktop'); } if (config.version !== '0.1.0') { throw new Error('desktop shell app version must be 0.1.0'); } if (packageConfig.version !== config.version) { throw new Error('desktop shell package version must match tauri.conf.json version'); } if (extractCargoPackageString(cargoManifest, 'name') !== 'genarrative-desktop-shell') { throw new Error('desktop shell Cargo package name must be genarrative-desktop-shell'); } if (extractCargoPackageString(cargoManifest, 'version') !== config.version) { throw new Error('desktop shell Cargo package version must match tauri.conf.json version'); } if (!main.includes('host_version: env!("CARGO_PKG_VERSION")')) { throw new Error('desktop shell runtime response must use the Cargo package version'); } 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.app?.windows ?? []).length !== 1) { throw new Error('desktop shell must expose exactly one configured main window'); } if (mainWindow.label !== 'main') { throw new Error('desktop shell main window label must be main'); } if (String(mainWindow.url ?? '').startsWith('http')) { throw new Error('desktop shell release window must load packaged H5 assets'); } if (!String(mainWindow.url ?? '').startsWith('index.html?')) { throw new Error('desktop shell release window must enter through packaged index.html'); } if (!String(config.build?.devUrl ?? '').startsWith('http://127.0.0.1:3000/?')) { throw new Error('desktop shell dev URL must load the local Vite H5 entry'); } if (mainWindow.devtools !== false) { throw new Error('desktop shell main WebView devtools must be explicitly disabled'); } assertDesktopIconSet(); if (config.bundle?.active !== true || config.bundle?.targets !== 'all') { throw new Error('desktop shell bundle targets must remain enabled for all platforms'); } const csp = String(config.app?.security?.csp ?? ''); const devCsp = String(config.app?.security?.devCsp ?? ''); for (const blockedCspToken of ["'unsafe-eval'", 'tauri:', 'file:']) { if (csp.includes(blockedCspToken)) { throw new Error(`desktop shell CSP must not include ${blockedCspToken}`); } if (devCsp.includes(blockedCspToken)) { throw new Error(`desktop shell dev CSP must not include ${blockedCspToken}`); } } for (const releaseOnlyBlockedCspToken of [ 'http://127.0.0.1', 'ws://127.0.0.1', ]) { if (csp.includes(releaseOnlyBlockedCspToken)) { throw new Error( `desktop shell release CSP must not include ${releaseOnlyBlockedCspToken}`, ); } } for (const requiredCspToken of [ "default-src 'self'", "script-src 'self'", "style-src 'self' 'unsafe-inline'", 'connect-src', 'frame-src', ]) { if (!csp.includes(requiredCspToken)) { throw new Error(`desktop shell CSP missing ${requiredCspToken}`); } } for (const requiredDevCspToken of [ 'http://127.0.0.1:*', 'ws://127.0.0.1:*', "script-src 'self'", ]) { if (!devCsp.includes(requiredDevCspToken)) { throw new Error(`desktop shell dev CSP missing ${requiredDevCspToken}`); } } 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}`); } } if (resolveHostVersionFromUrl(mainWindow.url) !== config.version) { throw new Error('desktop shell main window hostVersion must match tauri.conf.json version'); } if (resolveHostVersionFromUrl(config.build?.devUrl ?? '') !== config.version) { throw new Error('desktop shell dev hostVersion must match tauri.conf.json version'); } assertSameList( resolveHostCapabilitiesFromUrl(mainWindow.url), desktopCapabilities, 'desktop shell main window hostCapabilities', ); assertSameList( resolveHostCapabilitiesFromUrl(config.build?.devUrl ?? ''), desktopCapabilities, 'desktop shell dev hostCapabilities', ); for (const capability of sdkBackedCapabilities) { if ( resolveHostCapabilitiesFromUrl(mainWindow.url).includes(capability) || resolveHostCapabilitiesFromUrl(config.build?.devUrl ?? '').includes(capability) ) { throw new Error( `desktop shell URL must not advertise ${capability} without a real SDK/channel flow`, ); } } const allowedPermissions = [ 'core:default', 'allow-host-bridge-request', ]; const allowedTauriCommands = ['host_bridge_request']; const requiredMainSnippets = [ 'tauri_plugin_single_instance::init', 'resolve_desktop_single_instance_action', 'tauri_plugin_clipboard_manager::init()', 'TrayIconBuilder::with_id', 'register_desktop_tray(app)', 'DESKTOP_TRAY_ID', 'TRAY_MENU_SHOW', 'TRAY_MENU_RELOAD', 'TRAY_MENU_QUIT', 'show_main_window', 'reload_main_window', 'register_desktop_window_close_events', 'resolve_desktop_window_close_action', 'WindowEvent::CloseRequested', 'api.prevent_close()', 'close_window.hide()', 'desktop tray registration failed', '"appearance.getColorScheme"', '"app.lifecycle"', '"network.status"', '"network.statusChanged"', '"share.open"', '"share.setTarget"', '"navigation.openNativePage"', '"app.reloadWebView"', '"app.setTitle"', '"app.setBadgeCount"', '"clipboard.writeText"', '"clipboard.readText"', '"file.exportText"', '"file.importText"', '"file.exportImage"', '"file.importImage"', '"file.importAudio"', '"file.exportAudio"', '"file.imageDropped"', '"notification.showLocal"', 'tauri_plugin_dialog::init()', 'tauri_plugin_notification::init()', 'tauri_plugin_notification::NotificationExt', '"copied_to_clipboard"', '"file export cancelled"', '"file import cancelled"', 'BASE64_STANDARD.decode', 'blocking_pick_file', 'import_text_file_payload', 'import_image_file_payload', 'import_audio_file_payload', 'export_audio_payload', 'set_title', 'set_badge_count', 'window.reload()', 'read_text()', 'normalize_clipboard_text', 'window.theme()', 'WindowEvent::Focused', 'WindowEvent::DragDrop', 'host_bridge_event_script', 'origin: window.location.origin', 'source: window', 'resolve_desktop_network_status', 'network.statusChanged', 'file.imageDropped', 'app.notification().builder()', 'desktop_entry_url_with_platform', 'desktop_window_config_with_runtime_platform', 'desktop_window_config_with_runtime_platform(config)', 'should_allow_desktop_webview_navigation', 'desktop_external_navigation_url', 'open_desktop_external_navigation', '.on_navigation(move |url|', '.on_new_window(move |url, _features|', 'DownloadEvent', 'should_allow_desktop_webview_download', 'DownloadEvent::Requested { .. } => false', '.on_download(|_webview, event| should_allow_desktop_webview_download(&event))', 'NewWindowResponse::Deny', 'HOST_BRIDGE_METHODS', 'HOST_BRIDGE_REQUEST_ID_MAX_LENGTH', 'normalize_request_id', 'is_host_bridge_method', 'invalid host bridge request id', 'invalid host bridge method', 'HostBridgeReplayState', 'HostBridgeReplayReservation', 'HOST_BRIDGE_RESPONSE_CACHE_MAX', '.manage(HostBridgeReplayState::default())', 'replay_state.reserve(&request.id)', 'HostBridgeReplayState::wait_for_response', 'execute_host_bridge_request(app, request).await', 'replay_state.complete(slot, response)', ]; assertSameList( capability.windows ?? [], ['main'], 'desktop shell capability windows', ); assertOnlyMainCapabilityFile(); if (capability.identifier !== 'main') { throw new Error('desktop shell capability identifier must be main'); } if (String(capability.description ?? '').length === 0) { throw new Error('desktop shell capability description must be explicit'); } assertSameList( capability.permissions ?? [], allowedPermissions, 'desktop shell capability permissions', ); assertSameList( extractTauriBuildCommands(buildScript), allowedTauriCommands, 'desktop shell build manifest commands', ); assertSameList( extractTauriInvokeCommands(main), allowedTauriCommands, 'desktop shell invoke handler commands', ); assertGeneratedPermissions(allowedTauriCommands); if (buildScript.includes('resolve_desktop_shell_runtime')) { throw new Error('desktop shell build manifest exposes an unused runtime command'); } if (!cargoManifest.includes('features = ["tray-icon"]')) { throw new Error('desktop shell must enable the Tauri tray-icon feature'); } if (cargoManifest.includes('"devtools"')) { throw new Error('desktop shell must not enable Tauri devtools feature'); } if (!cargoManifest.includes('tauri-plugin-single-instance = "2.4.2"')) { throw new Error('desktop shell must depend on tauri-plugin-single-instance'); } if ( cargoManifest.includes('tauri-plugin-updater') || JSON.stringify(config).includes('updater') ) { throw new Error('desktop shell must not configure updater without real signing and endpoint'); } if ( main.indexOf('tauri_plugin_single_instance::init') > main.indexOf('tauri_plugin_clipboard_manager::init()') ) { throw new Error('desktop shell must register the single-instance plugin first'); } if (main.includes('single-instance",') || main.includes('"single-instance"')) { throw new Error('desktop shell must not emit secondary-instance argv to H5'); } for (const snippet of requiredMainSnippets) { if (!main.includes(snippet)) { throw new Error(`desktop shell Rust host bridge missing ${snippet}`); } }