mirror of
https://github.com/pestphp/pest.git
synced 2026-06-05 02:52:12 +02:00
Compare commits
4 Commits
81bfdbf8fe
...
b944ee5841
| Author | SHA1 | Date | |
|---|---|---|---|
| b944ee5841 | |||
| f4e22dcafe | |||
| 339c1e8cac | |||
| d4c7362132 |
@ -75,12 +75,22 @@ final readonly class ChangedFiles
|
||||
}
|
||||
|
||||
if (! $exists) {
|
||||
// Missing now. If the snapshot recorded it as absent too
|
||||
// (sentinel ''), state is identical to last run — unchanged.
|
||||
// Otherwise it was present last run and got deleted since.
|
||||
if ($snapshot !== '') {
|
||||
$remaining[] = $file;
|
||||
}
|
||||
// Missing on disk. We always invalidate here, even when
|
||||
// the snapshot also recorded "deleted" (sentinel '').
|
||||
// The `snapshot=='' && !exists` shortcut would in
|
||||
// principle say "no change since last run, cached
|
||||
// result is still valid" — but it's only safe if the
|
||||
// cached result was recorded *during* a run that saw
|
||||
// the file as deleted. A previous run that captured
|
||||
// the deletion in `lastRunTree` but failed to refresh
|
||||
// the cached pass/fail (paratest worker race, an
|
||||
// earlier plugin bug, etc.) would leave the cache
|
||||
// stuck on a stale pass from before the deletion.
|
||||
// Skipping invalidation in that state perpetuates the
|
||||
// wrong result on every subsequent run. Treat any
|
||||
// missing file as a change; cost is one re-run per
|
||||
// `--tia` while the file stays deleted.
|
||||
$remaining[] = $file;
|
||||
|
||||
continue;
|
||||
}
|
||||
@ -94,28 +104,21 @@ final readonly class ChangedFiles
|
||||
}
|
||||
|
||||
if ($hash === $snapshot) {
|
||||
// Same state as the last TIA invocation — unchanged.
|
||||
// Same state as the last TIA invocation — cached
|
||||
// result is still valid, no need to re-run.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Differs from the snapshot, but may still be a revert back
|
||||
// to the committed version (scenario: last run had an edit,
|
||||
// this run reverted it). Skipping this check causes stale
|
||||
// snapshots from previous scenarios to cascade into the
|
||||
// current run's invalidation set. Cheap to verify via
|
||||
// `git show <sha>:<path>`.
|
||||
if ($sha !== null && $sha !== '') {
|
||||
$baselineContent = $this->contentAtSha($sha, $file);
|
||||
|
||||
if ($baselineContent !== null) {
|
||||
$baselineHash = ContentHash::ofContent($file, $baselineContent);
|
||||
|
||||
if ($hash === $baselineHash) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Differs from the snapshot. This includes the
|
||||
// revert-back-to-baseline case (last run had a real edit
|
||||
// and was cached against that edit; this run reverted).
|
||||
// Even though the file now matches what's at the recorded
|
||||
// SHA, the cached test result reflects the *modified*
|
||||
// version, not the baseline version — so it's stale and
|
||||
// the test must re-run to refresh the cache. An earlier
|
||||
// version of this filter short-circuited on
|
||||
// matches-baseline, which served the stale failure
|
||||
// forever after the user reverted.
|
||||
$remaining[] = $file;
|
||||
}
|
||||
|
||||
|
||||
@ -829,36 +829,45 @@ final class Graph
|
||||
|
||||
/**
|
||||
* Maps a project-relative path to its Inertia component name if it
|
||||
* lives under `resources/js/Pages/` with a recognised framework
|
||||
* extension. Returns null otherwise so callers can cheaply ignore
|
||||
* non-page files. Matches Inertia's resolver convention: strip the
|
||||
* `resources/js/Pages/` prefix, strip the extension, preserve the
|
||||
* remaining slashes (`Users/Show.vue` → `Users/Show`).
|
||||
* lives under the project's pages directory with a recognised
|
||||
* framework extension. Returns null otherwise so callers can
|
||||
* cheaply ignore non-page files. Matches Inertia's resolver
|
||||
* convention: strip the pages prefix, strip the extension, preserve
|
||||
* the remaining slashes (`Users/Show.vue` → `Users/Show`).
|
||||
*
|
||||
* Both `resources/js/Pages/` (the classic Inertia-Vue convention)
|
||||
* and `resources/js/pages/` (the Laravel React starter kit, and
|
||||
* other lowercase-by-default setups) are accepted — paths from
|
||||
* git are case-sensitive on Linux, so we must match the exact
|
||||
* casing used by the project rather than picking one and forcing
|
||||
* the other to fall through to the broad watch pattern.
|
||||
*/
|
||||
private function componentForInertiaPage(string $rel): ?string
|
||||
{
|
||||
$prefix = 'resources/js/Pages/';
|
||||
foreach (['resources/js/Pages/', 'resources/js/pages/'] as $prefix) {
|
||||
if (! str_starts_with($rel, $prefix)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! str_starts_with($rel, $prefix)) {
|
||||
return null;
|
||||
$tail = substr($rel, strlen($prefix));
|
||||
$dot = strrpos($tail, '.');
|
||||
|
||||
if ($dot === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$extension = substr($tail, $dot + 1);
|
||||
|
||||
if (! in_array($extension, ['vue', 'tsx', 'jsx', 'svelte', 'ts', 'js'], true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$name = substr($tail, 0, $dot);
|
||||
|
||||
return $name === '' ? null : $name;
|
||||
}
|
||||
|
||||
$tail = substr($rel, strlen($prefix));
|
||||
$dot = strrpos($tail, '.');
|
||||
|
||||
if ($dot === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$extension = substr($tail, $dot + 1);
|
||||
|
||||
if (! in_array($extension, ['vue', 'tsx', 'jsx', 'svelte', 'ts', 'js'], true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$name = substr($tail, 0, $dot);
|
||||
|
||||
return $name === '' ? null : $name;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -33,7 +33,15 @@ final class InertiaEdges
|
||||
{
|
||||
private const string CONTAINER_CLASS = '\\Illuminate\\Container\\Container';
|
||||
|
||||
private const string REQUEST_HANDLED_EVENT = '\\Illuminate\\Foundation\\Http\\Events\\RequestHandled';
|
||||
/**
|
||||
* Event class name used as the listener key. Stored *without* a
|
||||
* leading backslash because Laravel's `Dispatcher` keys
|
||||
* `$listeners[$eventName]` by the literal string passed to
|
||||
* `listen()`, and looks up incoming events by their PHP-class
|
||||
* name (`get_class($event)`), which never has a leading
|
||||
* backslash. A `\Illuminate\…` key would silently never match.
|
||||
*/
|
||||
private const string REQUEST_HANDLED_EVENT = 'Illuminate\\Foundation\\Http\\Events\\RequestHandled';
|
||||
|
||||
/**
|
||||
* App-scoped marker that makes `arm()` idempotent across per-test
|
||||
@ -129,22 +137,66 @@ final class InertiaEdges
|
||||
}
|
||||
}
|
||||
|
||||
// Initial-load HTML path: Inertia embeds the page payload in a
|
||||
// `data-page` attribute on the root `<div id="app">`. We only
|
||||
// pay the regex cost when the body actually contains the
|
||||
// attribute, so non-Inertia HTML responses are effectively a
|
||||
// no-op.
|
||||
// Initial-load HTML path. Inertia ships two shapes here and
|
||||
// we honour both:
|
||||
//
|
||||
// 1. SSR-safe script tag — `<script data-page="app"
|
||||
// type="application/json">{…JSON…}</script>`. The
|
||||
// Laravel React starter kit (and modern Inertia-React)
|
||||
// use this so the JSON survives server-rendered
|
||||
// hydration without HTML-encoding the payload into an
|
||||
// attribute. The `data-page="app"` *attribute value* is
|
||||
// the literal string `"app"` — only the tag *body*
|
||||
// carries the page JSON.
|
||||
// 2. Classic — `<div id="app" data-page="{…JSON…}">…`. Older
|
||||
// Inertia-Vue and Inertia-React still emit this. Here
|
||||
// `data-page` IS the JSON, HTML-entity-encoded.
|
||||
//
|
||||
// Try the script-tag shape first; if the response uses it,
|
||||
// the classic regex would also see a `data-page="app"` token
|
||||
// and try to JSON-decode the literal string `"app"`.
|
||||
$content = self::readContent($response);
|
||||
|
||||
if ($content === null || ! str_contains($content, 'data-page=')) {
|
||||
if ($content === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preg_match('/\sdata-page="([^"]+)"/', $content, $match) !== 1) {
|
||||
return null;
|
||||
// Lookahead pair handles arbitrary attribute order on the
|
||||
// `<script>` tag.
|
||||
if (str_contains($content, 'type="application/json"')
|
||||
&& preg_match('#<script\b(?=[^>]*\bdata-page="app")(?=[^>]*\btype="application/json")[^>]*>(.+?)</script>#s', $content, $match) === 1) {
|
||||
$component = self::componentFromJson(html_entity_decode($match[1]));
|
||||
|
||||
if ($component !== null) {
|
||||
return $component;
|
||||
}
|
||||
}
|
||||
|
||||
$decoded = json_decode(html_entity_decode($match[1]), true);
|
||||
// Classic: only accept a value that looks like a JSON object
|
||||
// (`{…}`). Avoids matching the script-tag form's
|
||||
// `data-page="app"` attribute when both shapes coexist.
|
||||
if (str_contains($content, 'data-page=')
|
||||
&& preg_match('/\sdata-page="(\{[^"]+\})"/', $content, $match) === 1) {
|
||||
$component = self::componentFromJson(html_entity_decode($match[1]));
|
||||
|
||||
if ($component !== null) {
|
||||
return $component;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an Inertia page JSON blob and returns the `component`
|
||||
* field if it's a non-empty string. Used by both the script-tag
|
||||
* and the `data-page`-attribute paths so the success criteria are
|
||||
* identical.
|
||||
*/
|
||||
private static function componentFromJson(string $json): ?string
|
||||
{
|
||||
/** @var mixed $decoded */
|
||||
$decoded = json_decode($json, true);
|
||||
|
||||
if (is_array($decoded)
|
||||
&& isset($decoded['component'])
|
||||
|
||||
@ -36,23 +36,32 @@ final class JsImportParser
|
||||
|
||||
private const array RESOLVABLE_EXTENSIONS = ['vue', 'tsx', 'jsx', 'svelte', 'ts', 'js', 'mjs', 'mts'];
|
||||
|
||||
private const string PAGES_DIR = 'resources/js/Pages';
|
||||
|
||||
private const string JS_DIR = 'resources/js';
|
||||
|
||||
/**
|
||||
* Walks `resources/js/Pages` and, for each page, collects its
|
||||
* transitive file imports. Returns the inverted graph so callers
|
||||
* can look up "what pages depend on this shared file".
|
||||
* Walks the project's pages directory (`resources/js/Pages` or its
|
||||
* lowercase Laravel-React-starter-kit equivalent `resources/js/pages`)
|
||||
* and, for each page, collects its transitive file imports. Returns
|
||||
* the inverted graph so callers can look up "what pages depend on
|
||||
* this shared file".
|
||||
*
|
||||
* @return array<string, list<string>>
|
||||
*/
|
||||
public static function parse(string $projectRoot): array
|
||||
{
|
||||
$jsRoot = $projectRoot.DIRECTORY_SEPARATOR.self::JS_DIR;
|
||||
$pagesRoot = $projectRoot.DIRECTORY_SEPARATOR.self::PAGES_DIR;
|
||||
$pagesRoot = null;
|
||||
|
||||
if (! is_dir($pagesRoot)) {
|
||||
foreach (['resources/js/Pages', 'resources/js/pages'] as $candidate) {
|
||||
$abs = $projectRoot.DIRECTORY_SEPARATOR.$candidate;
|
||||
if (is_dir($abs)) {
|
||||
$pagesRoot = $abs;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($pagesRoot === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
@ -76,7 +76,22 @@ final class JsModuleGraph
|
||||
*/
|
||||
public static function isApplicable(string $projectRoot): bool
|
||||
{
|
||||
return self::hasViteConfig($projectRoot) && is_dir($projectRoot.DIRECTORY_SEPARATOR.'resources'.DIRECTORY_SEPARATOR.'js'.DIRECTORY_SEPARATOR.'Pages');
|
||||
if (! self::hasViteConfig($projectRoot)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Both the classic Inertia-Vue (`Pages/`) and the Laravel React
|
||||
// starter kit (`pages/`) conventions are accepted — projects
|
||||
// running on a case-sensitive filesystem (Linux CI) get
|
||||
// exactly one of the two, and we shouldn't refuse to walk the
|
||||
// graph based on which one it picks.
|
||||
foreach (['Pages', 'pages'] as $dir) {
|
||||
if (is_dir($projectRoot.DIRECTORY_SEPARATOR.'resources'.DIRECTORY_SEPARATOR.'js'.DIRECTORY_SEPARATOR.$dir)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -104,7 +119,21 @@ final class JsModuleGraph
|
||||
return null;
|
||||
}
|
||||
|
||||
$process = new Process([$nodeBinary, $helperPath, $projectRoot], $projectRoot);
|
||||
// Tell the Node helper which casing this project uses for its
|
||||
// pages directory. The helper defaults to `resources/js/Pages`;
|
||||
// the Laravel React starter ships lowercase `resources/js/pages`,
|
||||
// and on a case-sensitive filesystem the helper would otherwise
|
||||
// walk a non-existent directory and emit an empty module graph.
|
||||
$env = [];
|
||||
foreach (['resources/js/Pages', 'resources/js/pages'] as $candidate) {
|
||||
if (is_dir($projectRoot.DIRECTORY_SEPARATOR.$candidate)) {
|
||||
$env['TIA_VITE_PAGES_DIR'] = $candidate;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$process = new Process([$nodeBinary, $helperPath, $projectRoot], $projectRoot, $env);
|
||||
$process->setTimeout(self::NODE_TIMEOUT_SECONDS);
|
||||
$process->run();
|
||||
|
||||
|
||||
@ -76,6 +76,38 @@ final class Recorder
|
||||
*/
|
||||
private array $classUsesDatabaseCache = [];
|
||||
|
||||
/**
|
||||
* Reverse map of project-local source file → list of class /
|
||||
* interface / trait names declared in it. Built incrementally as
|
||||
* tests run and new classes get autoloaded; consumed by
|
||||
* `linkSourceDependencies()` so a test's covered file's
|
||||
* declared classes can be walked for their interfaces, traits,
|
||||
* and parents (which the coverage driver doesn't capture
|
||||
* because interface declarations and empty traits emit no
|
||||
* executable bytecode).
|
||||
*
|
||||
* @var array<string, list<string>>
|
||||
*/
|
||||
private array $fileToClassNames = [];
|
||||
|
||||
/**
|
||||
* Names already folded into `$fileToClassNames`. Lets the
|
||||
* incremental refresher skip classes seen in a previous test.
|
||||
*
|
||||
* @var array<string, true>
|
||||
*/
|
||||
private array $indexedClassNames = [];
|
||||
|
||||
/**
|
||||
* Cached "files this class transitively depends on (interfaces,
|
||||
* traits, parent chain, parents' interfaces and traits)" for
|
||||
* project-local class names. Avoids re-walking the same
|
||||
* hierarchy on every test that touches the same class.
|
||||
*
|
||||
* @var array<string, list<string>>
|
||||
*/
|
||||
private array $classDependencyCache = [];
|
||||
|
||||
private bool $active = false;
|
||||
|
||||
private bool $driverChecked = false;
|
||||
@ -196,6 +228,16 @@ final class Recorder
|
||||
$this->perTestFiles[$this->currentTestFile][$sourceFile] = true;
|
||||
}
|
||||
|
||||
// Walk each covered class's interfaces / traits / parent chain
|
||||
// and link those files explicitly. Interface declarations have
|
||||
// no executable bytecode, so coverage drivers never emit lines
|
||||
// for them — without this walk, a signature change to an
|
||||
// interface like `Viewable` would leave the cached results of
|
||||
// every test that exercises an implementing class stale,
|
||||
// because the interface file never enters the graph through
|
||||
// the coverage path.
|
||||
$this->linkSourceDependencies(array_keys($data));
|
||||
|
||||
$this->currentTestFile = null;
|
||||
}
|
||||
|
||||
@ -225,6 +267,173 @@ final class Recorder
|
||||
$this->perTestFiles[$this->currentTestFile][$sourceFile] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* For each project-local source file the coverage driver
|
||||
* captured for this test, finds the classes / interfaces / traits
|
||||
* declared in it and links every file in their declarative
|
||||
* hierarchy: implemented interfaces (transitive), used traits,
|
||||
* and parent classes (with their own interfaces and traits).
|
||||
*
|
||||
* Coverage drivers only record executable lines, so an interface
|
||||
* signature change (e.g. adding a return type to a `Viewable`
|
||||
* method) never registers — the interface file has no bytecode
|
||||
* to instrument. Without this walk, every class implementing the
|
||||
* interface would silently keep its stale cached result through
|
||||
* the change, even though `--parallel` (no TIA) catches the
|
||||
* incompatibility immediately.
|
||||
*
|
||||
* @param array<int, string> $coveredFiles absolute paths from coverage
|
||||
*/
|
||||
private function linkSourceDependencies(array $coveredFiles): void
|
||||
{
|
||||
if ($this->currentTestFile === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->refreshClassMap();
|
||||
|
||||
foreach ($coveredFiles as $coveredFile) {
|
||||
if (! isset($this->fileToClassNames[$coveredFile])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($this->fileToClassNames[$coveredFile] as $name) {
|
||||
foreach ($this->classDependencies($name) as $depFile) {
|
||||
$this->perTestFiles[$this->currentTestFile][$depFile] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Incrementally folds every project-local class / interface /
|
||||
* trait declared since the last refresh into `$fileToClassNames`.
|
||||
* PHP only ever appends to its declared-symbol lists (classes
|
||||
* never get unloaded), so iterating from `$indexedClassNames`'s
|
||||
* cardinality forward is sufficient — and over a long suite this
|
||||
* is dominated by the first test, since most classes are loaded
|
||||
* by then.
|
||||
*/
|
||||
private function refreshClassMap(): void
|
||||
{
|
||||
$names = array_merge(
|
||||
get_declared_classes(),
|
||||
get_declared_interfaces(),
|
||||
get_declared_traits(),
|
||||
);
|
||||
|
||||
foreach ($names as $name) {
|
||||
if (isset($this->indexedClassNames[$name])) {
|
||||
continue;
|
||||
}
|
||||
$this->indexedClassNames[$name] = true;
|
||||
|
||||
// Names came directly from `get_declared_*`, so the
|
||||
// class/interface/trait is guaranteed loaded — but
|
||||
// `class_exists($name, false)` (no autoload) keeps the
|
||||
// string narrowed to `class-string` for static analysis
|
||||
// and the `ReflectionClass` constructor stays in its
|
||||
// documented happy path.
|
||||
if (! class_exists($name, false)
|
||||
&& ! interface_exists($name, false)
|
||||
&& ! trait_exists($name, false)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$reflection = new ReflectionClass($name);
|
||||
|
||||
if ($reflection->isInternal()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$file = $reflection->getFileName();
|
||||
|
||||
if (! is_string($file)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str_contains($file, DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->fileToClassNames[$file][] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the project-local files the named class declaratively
|
||||
* depends on: implemented interfaces (transitive), used traits,
|
||||
* and the entire parent chain (each with their own interfaces
|
||||
* and traits). Cached per class because the answer is invariant
|
||||
* across a single process.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
private function classDependencies(string $className): array
|
||||
{
|
||||
if (isset($this->classDependencyCache[$className])) {
|
||||
return $this->classDependencyCache[$className];
|
||||
}
|
||||
|
||||
if (! class_exists($className, false)
|
||||
&& ! interface_exists($className, false)
|
||||
&& ! trait_exists($className, false)) {
|
||||
return $this->classDependencyCache[$className] = [];
|
||||
}
|
||||
|
||||
$reflection = new ReflectionClass($className);
|
||||
|
||||
$files = [];
|
||||
|
||||
$linkSymbol = static function (string $name) use (&$files): void {
|
||||
if (! class_exists($name, false)
|
||||
&& ! interface_exists($name, false)
|
||||
&& ! trait_exists($name, false)) {
|
||||
return;
|
||||
}
|
||||
$r = new ReflectionClass($name);
|
||||
if ($r->isInternal()) {
|
||||
return;
|
||||
}
|
||||
$f = $r->getFileName();
|
||||
if (! is_string($f) || str_contains($f, DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR)) {
|
||||
return;
|
||||
}
|
||||
$files[$f] = true;
|
||||
};
|
||||
|
||||
// `getInterfaceNames()` is transitive — it returns interfaces
|
||||
// from parent classes and parent interfaces too — so a single
|
||||
// pass covers the whole interface graph.
|
||||
foreach ($reflection->getInterfaceNames() as $iname) {
|
||||
$linkSymbol($iname);
|
||||
}
|
||||
|
||||
// Direct + ancestor traits. `getTraitNames()` doesn't recurse
|
||||
// into traits-using-traits, but that's a rare pattern in
|
||||
// application code; if a project genuinely needs it, the
|
||||
// coverage driver will pick up the executed bytecode of the
|
||||
// outer trait and the dependency walk runs against the
|
||||
// resulting class anyway.
|
||||
foreach ($reflection->getTraitNames() as $tname) {
|
||||
$linkSymbol($tname);
|
||||
}
|
||||
|
||||
$parent = $reflection->getParentClass();
|
||||
while ($parent !== false && ! $parent->isInternal()) {
|
||||
$f = $parent->getFileName();
|
||||
if (is_string($f) && ! str_contains($f, DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR)) {
|
||||
$files[$f] = true;
|
||||
}
|
||||
foreach ($parent->getTraitNames() as $tname) {
|
||||
$linkSymbol($tname);
|
||||
}
|
||||
$parent = $parent->getParentClass();
|
||||
}
|
||||
|
||||
return $this->classDependencyCache[$className] = array_keys($files);
|
||||
}
|
||||
|
||||
/**
|
||||
* Records every project-local ancestor class's defining file as a
|
||||
* source dependency of the currently-running test. PCOV / Xdebug
|
||||
@ -473,6 +682,9 @@ final class Recorder
|
||||
$this->perTestUsesDatabase = [];
|
||||
$this->classFileCache = [];
|
||||
$this->classUsesDatabaseCache = [];
|
||||
$this->fileToClassNames = [];
|
||||
$this->indexedClassNames = [];
|
||||
$this->classDependencyCache = [];
|
||||
$this->active = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -118,6 +118,11 @@ final class TableTracker
|
||||
return;
|
||||
}
|
||||
|
||||
$events->listen('\\Illuminate\\Database\\Events\\QueryExecuted', $listener);
|
||||
// Event class key intentionally has no leading backslash —
|
||||
// `Dispatcher::listen()` stores by the literal string and the
|
||||
// lookup at dispatch time uses `get_class($event)` (no
|
||||
// leading backslash), so a `\Illuminate\…` key would never
|
||||
// match the fired event.
|
||||
$events->listen('Illuminate\\Database\\Events\\QueryExecuted', $listener);
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,37 +30,39 @@ final readonly class Inertia implements WatchDefault
|
||||
? $testPath.'/Browser'
|
||||
: $testPath;
|
||||
|
||||
return [
|
||||
// Inertia page components (React / Vue / Svelte). Scoped to
|
||||
// `$browserDir` only — a Vue/React edit cannot change the
|
||||
// output of a server-side Inertia test (those assert on the
|
||||
// component *name* returned by `Inertia::render()`, not its
|
||||
// client-side implementation). Broad invalidation is only
|
||||
// meaningful for tests that actually render the DOM. Precise
|
||||
// per-component edges come from `InertiaEdges` at record
|
||||
// time and replace this fallback when available.
|
||||
'resources/js/Pages/**/*.vue' => [$browserDir],
|
||||
'resources/js/Pages/**/*.tsx' => [$browserDir],
|
||||
'resources/js/Pages/**/*.jsx' => [$browserDir],
|
||||
'resources/js/Pages/**/*.svelte' => [$browserDir],
|
||||
'resources/js/Pages/**/*.ts' => [$browserDir],
|
||||
'resources/js/Pages/**/*.js' => [$browserDir],
|
||||
// Inertia page components (React / Vue / Svelte). Scoped to
|
||||
// `$browserDir` only — a Vue/React edit cannot change the
|
||||
// output of a server-side Inertia test (those assert on the
|
||||
// component *name* returned by `Inertia::render()`, not its
|
||||
// client-side implementation). Broad invalidation is only
|
||||
// meaningful for tests that actually render the DOM. Precise
|
||||
// per-component edges come from `InertiaEdges` at record
|
||||
// time and replace this fallback when available.
|
||||
//
|
||||
// Both `Pages/` (classic Inertia-Vue) and `pages/` (Laravel
|
||||
// React starter kit, and other lowercase-by-default setups)
|
||||
// are emitted — paths from git are case-sensitive on Linux,
|
||||
// so a single casing would silently miss the other convention.
|
||||
$patterns = [];
|
||||
|
||||
// Shared layouts / components consumed by pages.
|
||||
'resources/js/Layouts/**/*.vue' => [$browserDir],
|
||||
'resources/js/Layouts/**/*.tsx' => [$browserDir],
|
||||
'resources/js/Layouts/**/*.ts' => [$browserDir],
|
||||
'resources/js/Layouts/**/*.js' => [$browserDir],
|
||||
'resources/js/Components/**/*.vue' => [$browserDir],
|
||||
'resources/js/Components/**/*.tsx' => [$browserDir],
|
||||
'resources/js/Components/**/*.ts' => [$browserDir],
|
||||
'resources/js/Components/**/*.js' => [$browserDir],
|
||||
foreach (['Pages', 'pages'] as $pages) {
|
||||
foreach (['vue', 'tsx', 'jsx', 'svelte', 'ts', 'js'] as $ext) {
|
||||
$patterns["resources/js/{$pages}/**/*.{$ext}"] = [$browserDir];
|
||||
}
|
||||
}
|
||||
|
||||
// SSR entry point.
|
||||
'resources/js/ssr.js' => [$browserDir],
|
||||
'resources/js/ssr.ts' => [$browserDir],
|
||||
'resources/js/app.js' => [$browserDir],
|
||||
'resources/js/app.ts' => [$browserDir],
|
||||
];
|
||||
foreach (['Layouts', 'layouts', 'Components', 'components'] as $shared) {
|
||||
foreach (['vue', 'tsx', 'ts', 'js'] as $ext) {
|
||||
$patterns["resources/js/{$shared}/**/*.{$ext}"] = [$browserDir];
|
||||
}
|
||||
}
|
||||
|
||||
// SSR entry point.
|
||||
$patterns['resources/js/ssr.js'] = [$browserDir];
|
||||
$patterns['resources/js/ssr.ts'] = [$browserDir];
|
||||
$patterns['resources/js/app.js'] = [$browserDir];
|
||||
$patterns['resources/js/app.ts'] = [$browserDir];
|
||||
|
||||
return $patterns;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user