fix(admin): verify the whole SPA build graph, not just the entry
find_missing_admin_chunks() only parsed index.js, but a lazily imported page chunk pulls in its own deps (shared-api, PageHeader, sortable-options…). A deploy that drops one of those still leaves the entry internally consistent, so the existing notice never fires and the failure only shows up when an admin opens that menu — every schemaPage keeps working, which makes the breakage look arbitrary. The same scan never looked at CSS at all: 18 stylesheets are reachable from the graph and none of them were checked, so a missing page stylesheet ships an unstyled screen with no warning. Walk the module graph transitively from index.js, queueing each discovered dependency, and match .js and .css specifiers. Deleting PageHeader now also reports every page that depends on it. This is a diagnostic hardening only — it makes an incomplete deploy loud instead of guessing at the cause of a broken page.
This commit is contained in:
1 parent
df12f51199
commit
324bcfdadc
3 files changed
+131
-12
No files matched your search
+52
-11
@@ -145,28 +145,69 @@ class Admin {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse index.js dynamic imports and report missing chunk files.
|
||||
* Walk the whole build/ module graph and report files a chunk asks for but
|
||||
* that are not on disk.
|
||||
*
|
||||
* @return string[]
|
||||
* Starting from index.js is not enough: a lazily imported page chunk pulls
|
||||
* in its own deps (shared-api, PageHeader, …), so a deploy that drops any
|
||||
* of those still leaves the entry consistent while the route 404s at
|
||||
* runtime. Traversing the graph transitively also catches CSS, which the
|
||||
* entry-only scan never verified — a missing page stylesheet silently
|
||||
* ships an unstyled admin screen.
|
||||
*
|
||||
* @return string[] Missing relative file names, e.g. "UsersBadge.a1b2c3.js".
|
||||
*/
|
||||
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 === '') {
|
||||
if (!is_file($entry_file)) {
|
||||
return ['index.js'];
|
||||
}
|
||||
|
||||
$entry_src = file_get_contents($entry_file);
|
||||
if (!is_string($entry_src) || $entry_src === '') {
|
||||
return ['index.js unreadable'];
|
||||
}
|
||||
|
||||
if (!preg_match_all('/\.\/([A-Za-z0-9_.-]+\.js)/', $src, $matches)) {
|
||||
return [];
|
||||
}
|
||||
// Relative specifiers the bundler emits: static `from "./x.js"`,
|
||||
// dynamic `import("./x.js")`, bare `import "./x.css"` and the
|
||||
// __vite__mapDeps lookup table.
|
||||
$specifier = '/\.\/([A-Za-z0-9_.-]+\.(?:js|css))/';
|
||||
|
||||
$missing = [];
|
||||
foreach (array_unique($matches[1]) as $chunk) {
|
||||
if (!is_file($build_dir . $chunk)) {
|
||||
$missing[] = $chunk;
|
||||
$visited = [];
|
||||
$queue = ['index.js'];
|
||||
|
||||
while ($queue !== []) {
|
||||
$file = array_shift($queue);
|
||||
if (isset($visited[$file])) {
|
||||
continue;
|
||||
}
|
||||
$visited[$file] = true;
|
||||
|
||||
$path = $build_dir . $file;
|
||||
// A .css entry in the graph is a leaf: nothing is imported from it.
|
||||
if (!is_file($path) || substr($file, -3) !== '.js') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$src = file_get_contents($path);
|
||||
if (!is_string($src) || $src === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!preg_match_all($specifier, $src, $matches)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (array_unique($matches[1]) as $dep) {
|
||||
if (!is_file($build_dir . $dep)) {
|
||||
$missing[$dep] = true;
|
||||
continue;
|
||||
}
|
||||
$queue[] = $dep;
|
||||
}
|
||||
}
|
||||
|
||||
return $missing;
|
||||
return array_keys($missing);
|
||||
}
|
||||
|
||||
public function add_module_type_attribute(string $tag, string $handle): string {
|
||||
|
||||
Reference in new issue
Block a user