diff --git a/src/RouteArtifactState.php b/src/RouteArtifactState.php index 84bd6d1..a5b95b0 100644 --- a/src/RouteArtifactState.php +++ b/src/RouteArtifactState.php @@ -8,21 +8,78 @@ class RouteArtifactState { public function store(string $type, array $artifact): void { + $previous = $this->loadPayload($type); $payload = [ 'timestamp' => time(), 'type' => $type, 'artifact' => $artifact, + 'build' => is_array($previous['build'] ?? null) ? $previous['build'] : null, ]; - $directory = dirname($this->getFilePath($type)); - if (!is_dir($directory)) { - mkdir($directory, 0755, true); - } - - file_put_contents($this->getFilePath($type), json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + $this->writePayload($type, $payload); } public function load(string $type): array + { + $data = $this->loadPayload($type); + return is_array($data['artifact'] ?? null) ? $data['artifact'] : []; + } + + public function markBuild(string $type, array $changedFiles): void + { + $data = $this->loadPayload($type); + if (!is_array($data['artifact'] ?? null)) { + return; + } + + $data['build'] = [ + 'generation' => time(), + 'timestamp' => time(), + 'changed_files' => $this->normalizeFiles($changedFiles), + ]; + + $this->writePayload($type, $data); + } + + public function clearBuild(string $type): void + { + $data = $this->loadPayload($type); + if (empty($data)) { + return; + } + + $data['build'] = null; + $this->writePayload($type, $data); + } + public function coversChangedFiles(string $type, array $changedFiles): bool + { + if (empty($changedFiles)) { + return false; + } + + $data = $this->loadPayload($type); + $build = is_array($data['build'] ?? null) ? $data['build'] : []; + $builtFiles = is_array($build['changed_files'] ?? null) ? $build['changed_files'] : []; + if (empty($builtFiles)) { + return false; + } + + $built = array_fill_keys($this->normalizeFiles($builtFiles), true); + foreach ($this->normalizeFiles($changedFiles) as $file) { + if (!isset($built[$file])) { + return false; + } + } + + return true; + } + + public function has(string $type): bool + { + return file_exists($this->getFilePath($type)); + } + + private function loadPayload(string $type): array { $file = $this->getFilePath($type); if (!file_exists($file)) { @@ -30,16 +87,47 @@ class RouteArtifactState } $data = json_decode((string)file_get_contents($file), true); - if (!is_array($data)) { - return []; - } - - return is_array($data['artifact'] ?? null) ? $data['artifact'] : []; + return is_array($data) ? $data : []; } - public function has(string $type): bool + private function writePayload(string $type, array $payload): void { - return file_exists($this->getFilePath($type)); + $file = $this->getFilePath($type); + $directory = dirname($file); + if (!is_dir($directory)) { + mkdir($directory, 0755, true); + } + + $tmpFile = $file . '.tmp.' . getmypid(); + $json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR); + if (file_put_contents($tmpFile, $json, LOCK_EX) === false) { + throw new \RuntimeException('Unable to write route artifact: ' . $tmpFile); + } + + if (@rename($tmpFile, $file)) { + return; + } + + // Windows rename() cannot replace an existing file. + if (is_file($file) && @unlink($file) && @rename($tmpFile, $file)) { + return; + } + + @unlink($tmpFile); + throw new \RuntimeException('Unable to publish route artifact: ' . $file); + } + + private function normalizeFiles(array $files): array + { + $normalized = []; + foreach ($files as $file) { + if (!is_string($file) || $file === '') { + continue; + } + $normalized[] = str_replace('\\', '/', realpath($file) ?: $file); + } + + return array_values(array_unique($normalized)); } private function getFilePath(string $type): string diff --git a/src/Router.php b/src/Router.php index b0de5a7..e544a06 100644 --- a/src/Router.php +++ b/src/Router.php @@ -214,6 +214,18 @@ class Router $changedFiles = $container->get(HotReloadState::class)->consume(); + $artifactState = $container->get(RouteArtifactState::class); + if (getenv('KIRI_FILE_BUILD') !== '1' && !empty($changedFiles) && $artifactState->coversChangedFiles(static::$type, $changedFiles)) { + $container->get(DataGrip::class)->reset(static::$type); + $router = $container->get(DataGrip::class)->get(static::$type); + if ($router->importArtifact($artifactState->load(static::$type))) { + $this->read_dir_file(APP_PATH . 'routes'); + $this->reset($container); + $coordinator->done(); + return; + } + } + // Worker 首次启动(无变更文件 + Master 已完成扫描): // 重新 include 路由文件(Router::get/post 显式注册) + 基于 Master 扫描清单重建注解路由 // 避免 opcache_compile_file,仅用 Reflection 重建路由,内存开销极小 @@ -232,7 +244,6 @@ class Router $container->get(DataGrip::class)->reset(static::$type); $scanner = $container->get(Kiri\Di\Scanner::class); - $artifactState = $container->get(RouteArtifactState::class); $scanConfig = array_merge( config('servers.reload.scan', []), config('site.scanner', []) @@ -241,12 +252,12 @@ class Router $normalizedAppPath = str_replace('\\', '/', realpath(APP_PATH . 'app') ?: APP_PATH . 'app'); $normalizedRoutePath = str_replace('\\', '/', realpath(APP_PATH . 'routes') ?: APP_PATH . 'routes'); - $routeChanged = false; + $routeChangedFiles = []; $appChangedFiles = []; foreach ($changedFiles as $changedFile) { if (str_starts_with($changedFile, $normalizedRoutePath . '/')) { - $routeChanged = true; + $routeChangedFiles[] = $changedFile; continue; } @@ -265,7 +276,7 @@ class Router if ($canUseArtifact) { $artifact = $artifactState->load(static::$type); $router = $container->get(DataGrip::class)->get(static::$type); - $usedArtifact = $router->importArtifact($artifact, $appChangedFiles); + $usedArtifact = $router->importArtifact($artifact, array_merge($appChangedFiles, $routeChangedFiles)); } // routes 目录中的显式路由文件必须每次重建路由表时重新 include。 diff --git a/src/RouterCollector.php b/src/RouterCollector.php index f1d65cd..1d38049 100644 --- a/src/RouterCollector.php +++ b/src/RouterCollector.php @@ -282,10 +282,6 @@ class RouterCollector implements \ArrayAccess, \IteratorAggregate public function importArtifact(array $artifact, array $excludeSourceFiles = []): bool { - if (($artifact['has_closure_routes'] ?? false) === true) { - return false; - } - $entries = $artifact['entries'] ?? null; if (!is_array($entries)) { return false;