Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 162 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,8 @@ function displayUser(user: App.Models.User) {
}
```

Attributes listed in `$hidden` are left out, as is anything marked with the `WayfinderIgnore` attribute — see [Leaving Things Out](#leaving-things-out).

## PHP Enums

Wayfinder converts PHP enums to TypeScript types and constants.
Expand Down Expand Up @@ -339,6 +341,8 @@ function setStatus(status: App.Enums.PostStatus) {
}
```

A case can be left out of the generated enum, either always or only for some builds — see [Leaving Things Out](#leaving-things-out).

### Enum Methods

Enums often carry methods alongside their cases:
Expand Down Expand Up @@ -616,13 +620,169 @@ This provides autocomplete and type-checking for `import.meta.env`:
const appName = import.meta.env.VITE_APP_NAME;
```

## Leaving Things Out

Some of what Wayfinder can see should not reach the browser. Mark it with the `WayfinderIgnore` attribute and nothing is generated for it:

```php
use Laravel\Wayfinder\Attributes\WayfinderIgnore;

#[WayfinderIgnore]
class InternalController
{
// No action file, no route helper, no request type.
}
```

The attribute works on a controller class or a single action, a model, an enum or one of its cases, a broadcast event or channel, and on a model's accessors and relations:

```php
class UserController
{
public function index() { /* generated */ }

#[WayfinderIgnore]
public function impersonate() { /* not generated */ }
}
```

```php
class User extends Model
{
#[WayfinderIgnore]
public function auditEntries(): HasMany
{
return $this->hasMany(AuditEntry::class);
}
}
```

Dropping an action drops everything that hangs off it: the route helper, the form variant, the page type, and the request type. Dropping a model drops relations that point at it, since there is no type left to point to.

### Keeping Something For Some Builds Only

Some declarations belong in local builds and nowhere else: a fake source provider, a seeding endpoint, a debug page. Pass `unless` and the declaration is kept only while the condition holds:

```php
// config/services.php
'fake_source_provider' => env('FAKE_SOURCE_PROVIDER', false),

// App\Enums\SourceProvider
enum SourceProvider: string
{
case Github = 'github';
case Gitlab = 'gitlab';

#[WayfinderIgnore(unless: 'services.fake_source_provider')]
case GitFake = 'gitfake';
}
```

Locally, with the flag on:

```typescript
export type SourceProvider = "github" | "gitlab" | "gitfake";
```

In production, with the flag off or missing:

```typescript
export type SourceProvider = "github" | "gitlab";
```

`when` is the other way round, for something to leave out while a condition holds rather than keep:

```php
class DebugController
{
#[WayfinderIgnore(when: 'services.hide_debug_tools')]
public function dump() { /* ... */ }
}
```

Either takes a config key, or a `[class, method]` callable for a condition with real logic behind it:

```php
#[WayfinderIgnore(unless: [SourceProviders::class, 'fakeEnabled'])]
```

Whatever you pass is answered when the files are generated, by the environment generating them. Wayfinder's output is regenerated per build, so each build gets the answer for where it runs. Nothing is remembered between builds: the analysis cache holds the condition, never the answer to it.

| Condition | `unless` | `when` |
| --------- | -------- | ------ |
| Passes | Kept | Left out |
| Fails, or the config key is missing | Left out | Kept |
| Cannot be read | Left out | Left out |

A condition Wayfinder cannot read leaves the declaration out whichever argument it was written with, so a typo shows up as a type error rather than shipping something. Only a config key or a callable counts as a condition, so `#[WayfinderIgnore(true)]` is not a way to switch a marker on or off — it is a marker with no condition, which leaves the declaration out. Passing both arguments is allowed and each can only add hiding, so `unless` keeping something does not override `when` leaving it out. Reach for one or the other.

One consequence worth planning for: the production build genuinely does not have the member, so code that reads it has to sit somewhere the production build never typechecks.

### Payload Keys

An attribute cannot go on an array key, so mark those with a comment instead. Put `@wayfinder-ignore` above the key or at the end of its line:

```php
return Inertia::render('Profile', [
'name' => $user->name,
'apiToken' => $user->api_token, // @wayfinder-ignore
'billing' => [
'plan' => $user->plan,
// @wayfinder-ignore
'stripeId' => $user->stripe_id,
],
]);
```

```typescript
export type Profile = Inertia.SharedData & {
name: string;
billing: { plan: string };
};
```

The key is gone, not emptied, so code that still reads it fails to compile. This works anywhere Wayfinder reads an array: page props, `toArray()` on a resource, `broadcastWith()`, and the rules of a form request.

A marker hides a member from its own type. If something else hands the same value out under a key of its own, that key needs its own marker.

For a plain database column on a model, Eloquent's `$hidden` and `#[Hidden]` already keep it out, and Wayfinder follows them.

### Traits

Nothing is generated for a trait, so a marker on the trait itself has nothing to hide. Mark the members inside it, which works whether the trait is used by one model or twenty:

```php
trait HasAvatar
{
#[WayfinderIgnore]
public function avatarPath(): Attribute { /* ... */ }
}
```

### Markers From Other Packages

To honor an attribute you cannot change, or a different comment tag, list them in `config/wayfinder.php`:

```php
'ignore' => [
'attributes' => [\Vendor\Package\Attributes\Internal::class],
'tags' => ['wayfinder-ignore', 'ignore'],
],
```

Your own attributes need no registration. Any attribute implementing `Laravel\Surveyor\Contracts\Ignored` is honored.

## Configuration

The configuration file is located at `config/wayfinder.php`:

```php
return [
'generate' => [
'ignore' => [
'attributes' => [],
'tags' => ['wayfinder-ignore'],
],
'route' => [
'actions' => env('WAYFINDER_GENERATE_ROUTE_ACTIONS', true),
'named' => env('WAYFINDER_GENERATE_NAMED_ROUTES', true),
Expand Down Expand Up @@ -660,6 +820,8 @@ return [

| Option | Description | Default |
| -------------------------------- | -------------------------------------- | ------------------------- |
| `generate.ignore.attributes` | Extra attributes that leave a declaration out | `[]` |
| `generate.ignore.tags` | Comment tags that leave an array key out | `['wayfinder-ignore']` |
| `generate.route.actions` | Generate controller action files | `true` |
| `generate.route.named` | Generate named route files | `true` |
| `generate.route.form_variant` | Include `.form` method variants | `true` |
Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
"illuminate/filesystem": "^12.0|^13.0",
"illuminate/routing": "^12.0|^13.0",
"illuminate/support": "^12.0|^13.0",
"laravel/ranger": "^0.4.0",
"laravel/surveyor": "^0.2.7",
"laravel/ranger": "^0.5.0",
"laravel/surveyor": "^0.3.0",
"phpstan/phpdoc-parser": "^2.3"
},
"require-dev": {
Expand Down
7 changes: 7 additions & 0 deletions config/wayfinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

return [
'generate' => [
// Leave declarations out of the generated files
'ignore' => [
// Attribute classes treated the same as Laravel\Wayfinder\Attributes\WayfinderIgnore
'attributes' => [],
// Comment tags that leave out the array key they sit on
'tags' => ['wayfinder-ignore'],
],
'route' => [
'actions' => env('WAYFINDER_GENERATE_ROUTE_ACTIONS', true),
'named' => env('WAYFINDER_GENERATE_NAMED_ROUTES', true),
Expand Down
44 changes: 44 additions & 0 deletions src/Attributes/WayfinderIgnore.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace Laravel\Wayfinder\Attributes;

use Attribute;
use Laravel\Surveyor\Contracts\ConditionallyIgnored;

/**
* Leave the marked declaration out of the generated TypeScript.
*
* On a class, nothing is generated for it at all. On a controller method, the
* route it handles is dropped along with its form variant, page type, and
* request type. On a property, accessor, relation, or enum case, that member is
* dropped from the type around it.
*
* Pass `unless` to keep it only while a condition holds, or `when` to leave it
* out only while one holds. Since generation runs per build, a condition is
* answered by the environment that generated the files. A condition Wayfinder
* cannot read leaves the declaration out either way.
*/
#[Attribute(Attribute::TARGET_ALL)]
final class WayfinderIgnore implements ConditionallyIgnored
{
/**
* @param string|array{0: class-string, 1: string}|null $unless Keep it while this passes: a config key or a [class, method] callable.
* @param string|array{0: class-string, 1: string}|null $when Leave it out while this passes.
*/
public function __construct(
public readonly string|array|null $unless = null,
public readonly string|array|null $when = null,
) {
//
}

public function unless(): string|array|null
{
return $this->unless;
}

public function when(): string|array|null
{
return $this->when;
}
}
19 changes: 19 additions & 0 deletions src/Console/GenerateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Laravel\Ranger\Support\Config as RangerConfig;
use Laravel\Ranger\Support\Inventory;
use Laravel\Surveyor\Analyzer\AnalyzedCache;
use Laravel\Surveyor\Support\Markers;
use Laravel\Wayfinder\Converters\BroadcastChannels;
use Laravel\Wayfinder\Converters\BroadcastEvents;
use Laravel\Wayfinder\Converters\Enums;
Expand Down Expand Up @@ -66,6 +67,8 @@ public function handle(
AnalyzedCache::setCacheDirectory($cacheDirectory);
RangerConfig::set('cache.directory', $cacheDirectory);

$this->registerIgnoreMarkers();

$cacheEnabled = $this->config->get('wayfinder.cache.enabled');

if ($this->option('fresh') || ! $cacheEnabled) {
Expand Down Expand Up @@ -153,6 +156,22 @@ public function handle(
$this->writeFiles();
}

/**
* Tell the analyzer which attributes and comment tags mean "leave this
* out", and fold them into the cache key: they decide what ends up in the
* generated files, so a cached run must not answer for a different set.
*/
protected function registerIgnoreMarkers(): void
{
$attributes = $this->config->get('wayfinder.generate.ignore.attributes', []);
$tags = $this->config->get('wayfinder.generate.ignore.tags', ['wayfinder-ignore']);

Markers::registerAttributes(...$attributes);
Markers::registerTags(...$tags);

AnalyzedCache::setKey(hash('sha256', serialize([$attributes, $tags])));
}

protected function getBasePaths(): array
{
if ($this->option('base-path')) {
Expand Down
Loading
Loading