manifest_path = $manifest_path ?? (defined('ZADEX_BUILD_PATH') ? ZADEX_BUILD_PATH . 'manifest.json' : ''); $this->dev_server = defined('ZADEX_DEV_SERVER') ? ZADEX_DEV_SERVER : $dev_server; } private string $manifest_path; public function is_dev(): bool { if (defined('ZADEX_DEV')) { return (bool) ZADEX_DEV; } return defined('ZADEX_PATH') && file_exists(ZADEX_PATH . '.vite-dev'); } private ?bool $dev_reachable = null; /** * Whether the Vite dev server accepts connections (cached per request, 500 ms timeout). A stale * `.vite-dev` marker or a forgotten ZADEX_DEV constant must never leave the editor blank. */ public function dev_server_reachable(): bool { if ($this->dev_reachable !== null) { return $this->dev_reachable; } $parts = wp_parse_url($this->dev_server); $host = isset($parts['host']) ? (string) $parts['host'] : 'localhost'; $port = isset($parts['port']) ? (int) $parts['port'] : (($parts['scheme'] ?? 'http') === 'https' ? 443 : 80); $socket = @stream_socket_client('tcp://' . $host . ':' . $port, $errno, $errstr, 0.5); $this->dev_reachable = is_resource($socket); if (is_resource($socket)) { fclose($socket); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- closes the TCP probe socket, not a file. } return $this->dev_reachable; } /** * Dev mode is only used when Vite answers; otherwise the production build (when present) is served so * the editor keeps working. With neither, print_entry() explains what to do instead of a blank page. */ public function uses_dev_server(): bool { return $this->is_dev() && ($this->dev_server_reachable() || !$this->has_build()); } /** Explanation printed by print_entry() when dev mode is on but nothing can be loaded ('' otherwise). */ public function dev_problem(): string { if (!$this->is_dev() || $this->dev_server_reachable() || $this->has_build()) { return ''; } return sprintf( /* translators: %s: dev server URL */ __('Zadex is in development mode (ZADEX_DEV or the .vite-dev marker file) but the Vite dev server at %s is not running and there is no production build. Run "npm run dev" or "npm run build", or remove the marker.', 'zadex'), $this->dev_server ); } public function manifest(): array { if ($this->manifest === null) { $this->manifest = []; if ($this->manifest_path && is_readable($this->manifest_path)) { $decoded = wp_json_file_decode($this->manifest_path, ['associative' => true]); if (is_array($decoded)) { $this->manifest = $decoded; } } } return $this->manifest; } public function has_build(): bool { return !empty($this->manifest()[self::ENTRY_EDITOR]['file']); } /** * Returns ['js' => [urls], 'css' => [urls]] for an entry, following imported chunks. */ public function entry(string $entry): array { if ($this->uses_dev_server()) { return [ 'js' => [$this->dev_server . '/@vite/client', $this->dev_server . '/' . $entry], 'css' => [], ]; } $manifest = $this->manifest(); $js = []; $css = []; $seen = []; $walk = function (string $key) use (&$walk, &$js, &$css, &$seen, $manifest): void { if (isset($seen[$key]) || empty($manifest[$key])) { return; } $seen[$key] = true; $chunk = $manifest[$key]; foreach ($chunk['imports'] ?? [] as $import) { $walk($import); } foreach ($chunk['css'] ?? [] as $file) { $css[] = ZADEX_BUILD_URL . $file; } // Only entries are loaded as scripts; shared chunks are pulled in through ES imports. if (!empty($chunk['file']) && !empty($chunk['isEntry'])) { $js[] = ZADEX_BUILD_URL . $chunk['file']; } }; $walk($entry); return ['js' => array_values(array_unique($js)), 'css' => array_values(array_unique($css))]; } /** Prints the stylesheets + module scripts of an entry through the WP APIs (used by the editor shell). */ public function print_entry(string $entry): void { $problem = $this->dev_problem(); if ($problem !== '') { // Nothing can be loaded: replace the (empty) editor root with the explanation once the body exists. $html = '