加固原生壳生产入口门禁
桌面壳深链打开失败改为显式日志记录 移动壳生产导出校验共享主站 URL 原生壳方案文档同步生产入口验收规则
This commit is contained in:
@@ -2104,6 +2104,10 @@ const requiredRustHostSnippets = [
|
||||
'app.deep_link().get_current()',
|
||||
'app.deep_link().register_all()',
|
||||
'normalize_desktop_deep_link_url',
|
||||
'log_desktop_deep_link_open_result',
|
||||
'open_desktop_deep_link_url(window: &WebviewWindow, url: &Url) -> tauri::Result<()>',
|
||||
'window.navigate(target_url)?',
|
||||
'show_main_window(window.app_handle())',
|
||||
'DESKTOP_DEEP_LINK_HOSTS',
|
||||
'resolve_desktop_single_instance_action',
|
||||
'tauri_plugin_clipboard_manager::init()',
|
||||
@@ -2709,6 +2713,8 @@ for (const blockedLifecycleSnippet of [
|
||||
'let _ = emit_current_desktop_lifecycle_event(window)',
|
||||
'let _ = emit_current_desktop_lifecycle_event(&lifecycle_window)',
|
||||
'let _ = emit_desktop_image_drop_event(&drop_window',
|
||||
'let _ = window.navigate(target_url)',
|
||||
'let _ = show_main_window(window.app_handle())',
|
||||
]) {
|
||||
if (rustHostSource.includes(blockedLifecycleSnippet)) {
|
||||
throw new Error(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::shell::lifecycle::log_desktop_host_event_result;
|
||||
use crate::shell::tray::show_main_window;
|
||||
use crate::shell::webview::{desktop_h5_url_with_host_context, WEB_APP_ORIGIN};
|
||||
use tauri::{Manager, Url, WebviewWindow};
|
||||
@@ -62,13 +63,17 @@ pub(crate) fn normalize_desktop_deep_link_url(raw_url: &Url) -> Option<Url> {
|
||||
desktop_h5_url_with_host_context(target_url)
|
||||
}
|
||||
|
||||
fn open_desktop_deep_link_url(window: &WebviewWindow, url: &Url) {
|
||||
fn open_desktop_deep_link_url(window: &WebviewWindow, url: &Url) -> tauri::Result<()> {
|
||||
let Some(target_url) = normalize_desktop_deep_link_url(url) else {
|
||||
return;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let _ = window.navigate(target_url);
|
||||
let _ = show_main_window(window.app_handle());
|
||||
window.navigate(target_url)?;
|
||||
show_main_window(window.app_handle())
|
||||
}
|
||||
|
||||
fn log_desktop_deep_link_open_result(result: tauri::Result<()>) -> bool {
|
||||
log_desktop_host_event_result("deep_link.open", result)
|
||||
}
|
||||
|
||||
pub(crate) fn register_desktop_deep_link_events(app: &tauri::App) -> tauri::Result<()> {
|
||||
@@ -79,7 +84,7 @@ pub(crate) fn register_desktop_deep_link_events(app: &tauri::App) -> tauri::Resu
|
||||
};
|
||||
|
||||
for url in event.urls() {
|
||||
open_desktop_deep_link_url(&window, &url);
|
||||
log_desktop_deep_link_open_result(open_desktop_deep_link_url(&window, &url));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -88,7 +93,7 @@ pub(crate) fn register_desktop_deep_link_events(app: &tauri::App) -> tauri::Resu
|
||||
};
|
||||
if let Ok(Some(urls)) = app.deep_link().get_current() {
|
||||
for url in urls {
|
||||
open_desktop_deep_link_url(&window, &url);
|
||||
log_desktop_deep_link_open_result(open_desktop_deep_link_url(&window, &url));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,4 +230,12 @@ mod tests {
|
||||
Some("genarrative")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn desktop_deep_link_open_result_reports_success_and_failure() {
|
||||
assert!(log_desktop_deep_link_open_result(Ok(())));
|
||||
assert!(!log_desktop_deep_link_open_result(Err(
|
||||
tauri::Error::AssetNotFound("deep-link".to_string())
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,43 @@ import fs from 'node:fs';
|
||||
|
||||
const shellRoot = new URL('../', import.meta.url);
|
||||
const outputRoot = new URL('../.expo-export-smoke/', import.meta.url);
|
||||
const hostBridgeContractUrl = new URL(
|
||||
'../../../packages/shared/src/contracts/hostBridge.ts',
|
||||
import.meta.url,
|
||||
);
|
||||
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
||||
const platforms = ['android', 'ios'];
|
||||
const blockedDevelopmentWebUrlPatterns = [
|
||||
/http:\\?\/\\?\/localhost(?::\d+)?/u,
|
||||
/http:\\?\/\\?\/127\.0\.0\.1(?::\d+)?/u,
|
||||
/http:\\?\/\\?\/\[::1\](?::\d+)?/u,
|
||||
];
|
||||
|
||||
function readHostBridgePublicWebUrl() {
|
||||
const contractSource = fs.readFileSync(hostBridgeContractUrl, 'utf8');
|
||||
const publicWebUrlMatch = contractSource.match(
|
||||
/HOST_BRIDGE_PUBLIC_WEB_URL\s*=\s*'([^']+)'/u,
|
||||
);
|
||||
const publicWebOriginMatch = contractSource.match(
|
||||
/HOST_BRIDGE_PUBLIC_WEB_ORIGIN\s*=\s*'([^']+)'/u,
|
||||
);
|
||||
|
||||
if (!publicWebUrlMatch || !publicWebOriginMatch) {
|
||||
throw new Error('HostBridge public web URL contract is missing');
|
||||
}
|
||||
|
||||
const publicWebUrl = publicWebUrlMatch[1];
|
||||
const publicWebOrigin = publicWebOriginMatch[1];
|
||||
if (new URL(publicWebUrl).origin !== publicWebOrigin) {
|
||||
throw new Error(
|
||||
'HostBridge public web URL and origin contract must point to the same origin',
|
||||
);
|
||||
}
|
||||
|
||||
return publicWebUrl;
|
||||
}
|
||||
|
||||
const expectedPublicWebUrl = readHostBridgePublicWebUrl();
|
||||
|
||||
function runExpoExport(platform) {
|
||||
const outputDir = `.expo-export-smoke/${platform}`;
|
||||
@@ -80,6 +115,21 @@ function assertBundle(platform, bundlePath) {
|
||||
if (!bundlePath.includes(`/static/js/${platform}/AppEntry-`)) {
|
||||
throw new Error(`Expo ${platform} bundle path does not target AppEntry`);
|
||||
}
|
||||
|
||||
const bundleSource = fs.readFileSync(bundleFile, 'utf8');
|
||||
if (!bundleSource.includes(expectedPublicWebUrl)) {
|
||||
throw new Error(
|
||||
`Expo ${platform} production bundle must include the shared public web URL`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const blockedPattern of blockedDevelopmentWebUrlPatterns) {
|
||||
if (blockedPattern.test(bundleSource)) {
|
||||
throw new Error(
|
||||
`Expo ${platform} production bundle must not include a local development H5 URL`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs.rmSync(outputRoot, {recursive: true, force: true});
|
||||
|
||||
Reference in New Issue
Block a user