diff --git a/.gitignore b/.gitignore
index ba0b509c..d129bf2d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,4 +21,8 @@ restore-helper-monoliths.php
# OS / editor noise
.DS_Store
Thumbs.db
-*.log
\ No newline at end of file
+*.log
+
+
+# Generated contract audit artifact
+tools/admin-contract-report.json
diff --git a/inc/classes/class-aksrt-admin.php b/inc/classes/class-aksrt-admin.php
index ebfb4909..25dcf8cb 100644
--- a/inc/classes/class-aksrt-admin.php
+++ b/inc/classes/class-aksrt-admin.php
@@ -66,6 +66,37 @@ class AKSRT_Admin {
$build_dir = trailingslashit(AKSRT_DIR) . 'assets/admin/build/';
$build_uri = trailingslashit(AKSRT_URI) . 'assets/admin/build/';
+ $entry = $build_dir . 'index.js';
+
+ if (!is_file($entry)) {
+ add_action('admin_notices', static function (): void {
+ if (!current_user_can('manage_options')) {
+ return;
+ }
+ echo '
';
+ echo esc_html__('AKSRT 管理后台构建缺失:请在 assets/admin 执行 npm run build,并完整部署 build/ 目录。', 'aksrt');
+ echo '
';
+ });
+ return;
+ }
+
+ $missing_chunks = self::find_missing_admin_chunks($build_dir, $entry);
+ if ($missing_chunks !== []) {
+ add_action('admin_notices', static function () use ($missing_chunks): void {
+ if (!current_user_can('manage_options')) {
+ return;
+ }
+ echo '';
+ echo esc_html__('AKSRT 管理后台构建不完整(动态模块缺失),请重新完整部署 assets/admin/build/,不要只覆盖 index.js。缺失:', 'aksrt');
+ echo ' ' . esc_html(implode(', ', array_slice($missing_chunks, 0, 8)));
+ if (count($missing_chunks) > 8) {
+ echo '…';
+ }
+ echo '
';
+ });
+ // Still enqueue entry so console shows the real import error if an admin insists;
+ // notice makes the deploy mistake explicit.
+ }
$css_files = [
'naive-ui.css',
@@ -84,28 +115,51 @@ class AKSRT_Admin {
}
}
- $entry = $build_dir . 'index.js';
- if (file_exists($entry)) {
- wp_enqueue_script(
- 'aksrt-admin-app',
- $build_uri . 'index.js',
- [],
- (string) filemtime($entry),
- true
- );
+ wp_enqueue_script(
+ 'aksrt-admin-app',
+ $build_uri . 'index.js',
+ [],
+ (string) filemtime($entry),
+ true
+ );
- wp_localize_script('aksrt-admin-app', 'aksrtAdminData', [
- 'apiUrl' => esc_url_raw(rest_url(self::API_NAMESPACE)),
- 'nonce' => wp_create_nonce('wp_rest'),
- 'siteUrl' => home_url('/'),
- 'adminUrl' => admin_url('/'),
- 'themeVersion' => wp_get_theme()->get('Version') ?: '3.4.0-dev',
- ]);
- }
+ wp_localize_script('aksrt-admin-app', 'aksrtAdminData', [
+ 'apiUrl' => esc_url_raw(rest_url(self::API_NAMESPACE)),
+ 'nonce' => wp_create_nonce('wp_rest'),
+ 'siteUrl' => home_url('/'),
+ 'adminUrl' => admin_url('/'),
+ 'themeVersion' => wp_get_theme()->get('Version') ?: '3.4.0-dev',
+ 'buildOk' => $missing_chunks === [],
+ ]);
wp_enqueue_media();
}
+ /**
+ * Parse index.js dynamic imports and report missing chunk files.
+ *
+ * @return string[]
+ */
+ private static function find_missing_admin_chunks(string $build_dir, string $entry_file): array {
+ $src = file_get_contents($entry_file);
+ if (!is_string($src) || $src === '') {
+ return ['index.js unreadable'];
+ }
+
+ if (!preg_match_all('/\\.\\/([A-Za-z0-9_.-]+\\.js)/', $src, $matches)) {
+ return [];
+ }
+
+ $missing = [];
+ foreach (array_unique($matches[1]) as $chunk) {
+ if (!is_file($build_dir . $chunk)) {
+ $missing[] = $chunk;
+ }
+ }
+
+ return $missing;
+ }
+
public function add_module_type_attribute(string $tag, string $handle): string {
if ('aksrt-admin-app' === $handle) {
return str_replace('