Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[1.1] Compile time rendering #21

Merged
merged 34 commits into from
Feb 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
467d3ea
CTR initial implementation
JohnathonKoster Jan 26, 2025
b0e50c2
Provide a way to opt out of compile time rendering entirely
JohnathonKoster Jan 26, 2025
3b3ad3a
Add infrastructure to avoid common CTR-unfriendly functions
JohnathonKoster Jan 26, 2025
bf2754d
Make list of functions config driven
JohnathonKoster Jan 27, 2025
9c1970e
Few more.
JohnathonKoster Jan 27, 2025
95250a6
Some vars that should disable CTR
JohnathonKoster Jan 27, 2025
168d0a1
Use a shared instance.
JohnathonKoster Jan 27, 2025
5a45455
Refactor to get rid of insane value passing
JohnathonKoster Jan 27, 2025
b9a1aee
Infrastructure to shutdown CTR based on static calls
JohnathonKoster Jan 27, 2025
333c05e
Cleanup
JohnathonKoster Jan 27, 2025
80d4a1f
Makes it clearer "where" those classes live.
JohnathonKoster Jan 27, 2025
5f67b2f
Refactor for clarity
JohnathonKoster Jan 27, 2025
52452b8
Move namespace
JohnathonKoster Jan 27, 2025
b85715e
Merge branch 'main' into compile-time-rendering
JohnathonKoster Jan 28, 2025
e9e0d33
Update TerminatingListener.php
JohnathonKoster Jan 28, 2025
a9ecd5f
Provide attributes to control behaviors
JohnathonKoster Jan 28, 2025
71a81ef
Built an escaper, just to be able to remove it lol
JohnathonKoster Jan 28, 2025
51daa64
Some code cleanup 🧹
JohnathonKoster Jan 29, 2025
fd7e020
Add test coverage for roots not pre-compiling ineligible nested compo…
JohnathonKoster Jan 29, 2025
b478f89
Update TemplateCompiler.php
JohnathonKoster Jan 29, 2025
6d2610c
Refactor to not have a hardcoded list
JohnathonKoster Jan 29, 2025
2780ae7
More test coverage
JohnathonKoster Jan 30, 2025
a795714
Coverage for additional "exit" style things
JohnathonKoster Jan 30, 2025
df2dfc6
Nope.
JohnathonKoster Jan 30, 2025
4fb5ad2
Additional test coverage
JohnathonKoster Feb 2, 2025
a9266b8
Merge branch 'main' into compile-time-rendering
JohnathonKoster Feb 2, 2025
5b69cd3
Make names more generic
JohnathonKoster Feb 8, 2025
9c4fbd5
Update CHANGELOG.md
JohnathonKoster Feb 8, 2025
5953f00
Consider default prop values when checking attributes
JohnathonKoster Feb 8, 2025
713a767
Some docs/more test coverage
JohnathonKoster Feb 8, 2025
5a10ff6
Update README.md
JohnathonKoster Feb 8, 2025
67c9851
Need more coffee
JohnathonKoster Feb 8, 2025
62848ff
Update README.md
JohnathonKoster Feb 8, 2025
c86abb1
Merge branch 'main' into compile-time-rendering
JohnathonKoster Feb 8, 2025
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- Adds a new "Compile Time Rendering" system, which can render components at compile time and inline the static output
- Adds compiler support for circular component references, like nested comment threads
- Adds a `#cache` compiler attribute, which may be used to cache the results of any Dagger component
- Bumps the minimum Laravel version to `11.23`, for `Cache::flexible` support
Expand Down
149 changes: 149 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ The main visual difference when working with Dagger components is the use of the
- [Dynamic Components](#dynamic-components)
- [Custom Component Paths and Namespaces](#custom-component-paths-and-namespaces)
- [Blade Component Prefix](#blade-component-prefix)
- [Compile Time Rendering](#compile-time-rendering)
- [Disabling Compile Time Rendering on a Component](#disabling-compile-time-rendering-on-a-component)
- [Enabling/Disabling Optimizations on Classes or Methods](#enablingdisabling-optimizations-on-classes-or-methods)
- [Notes on Compile Time Rendering](#notes-on-compile-time-rendering)
- [The View Manifest](#the-view-manifest)
- [License](#license)

Expand Down Expand Up @@ -1389,6 +1393,151 @@ Custom components can leverage all features of the Dagger compiler using their c

You are **not** allowed to register the prefix `x` with the Dagger compiler; attempting to do so will raise an `InvalidArgumentException`.

## Compile Time Rendering

The Dagger compiler contains a subsystem known as the Compile Time Renderer, or CTR. This checks to see if all the props on a component are resolvable at runtime; if so, it may elect to compile the component at runtime and insert the pre-rendered results into Blade's compiled output.

This feature has a number of internal safe guards, and here are a few of the things that will internally disable this feature:

- Dynamic/interpolated variable references
- Using Mixins
- Most static method calls
- Referencing PHP's [superglobals](https://php.net/superglobals), such as `$_GET` or `$_POST`
- Using debugging-related functions in a component, such as `dd`, `dump`, `var_dump`, etc.
- Calling functions such as `time`, `now`, or `date`
- Enabling the Attribute Cache on a component
- Components with slots

Imagine we have the following alert component:

```blade
<!-- /resources/dagger/views/alert.blade.php -->

@props(['type' => 'info', 'message'])

<div {{ $attributes->merge(['class' => 'alert alert-'.$type]) }}>
{{ $message }}
</div>
```

If we were to call the alert component like so:

```blade
<c-alert message="My awesome message" />
```

The compiler would detect that all props are resolvable, and the following would be emitted in the compiled Blade output:

```html
<div class="alert alert-info">
The awesome message
</div>
```

However, if we were to call our component like so, the compiler would not attempt to render the component at compile time:

```blade
<c-alert :$message />
```

### Disabling Compile Time Rendering on a Component

The CTR system should be transparent from a component author's point-of-view, however, if the rare event that you need to disable compiler optimizations, you may do so using the `compiler` helper method:

```blade
<!-- /resources/dagger/views/the_component.blade.php -->
@php
\Stillat\Dagger\component()
->props(['title'])
->compiler(
allowOptimizations: false
);
@endphp

{{ $title }}
```

If you find yourself disabling optimizations on a component, please open a discussion or an issue with details on which behaviors led to that decision.

### Enabling/Disabling Optimizations on Classes or Methods

The CTR system will aggressively disable itself whenever it detects static method calls within component templates. You may choose to mark these methods as safe using the `EnableOptimization` attribute:

```php
<?php

namespace App;

use Stillat\Dagger\Compiler\EnableOptimization;

class MyAwesomeClass
{

#[EnableOptimization]
public static function myHelper()
{

}

}
```

You may also mark an entire class as safe for optimizations by applying the attribute to the class instead:

```php
<?php

namespace App;

use Stillat\Dagger\Compiler\EnableOptimization;

#[EnableOptimization]
class MyAwesomeClass
{

public static function myHelper()
{

}

}
```

If you'd like to *disable* optimizations on a class or method explicitly, you may use the `DisableOptimization` attribute instead.

The following example would enable optimizations on the entire class but disable it on a specific method:

```php
<?php

namespace App;

use Stillat\Dagger\Compiler\DisableOptimization;
use Stillat\Dagger\Compiler\EnableOptimization;

#[EnableOptimization]
class MyAwesomeClass
{

public static function methodOne()
{
// Optimization allowed.
}

#[DisableOptimization]
public static function methodTwo()
{
// Optimization not allowed.
}

}
```

### Notes on Compile Time Rendering

- You should *never* attempt to force a component to render at compile time, outside of applying the `EnableOptimization` or `DisableOptimization` attributes to your own helper methods
- If an exception is raised while rendering a component at compile time, CTR will be disabled for that component and the compiler will revert to normal behavior

## The View Manifest

You may have noticed JSON files being written to your compiled view folder. These files are created by Dagger's "view manifest", which tracks dependencies of compiled views.
Expand Down
17 changes: 17 additions & 0 deletions config/dagger.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

return [
'ctr' => [
'unsafe_variables' => [
'$_GET', '$_POST', '$_FILES', '$_REQUEST', '$_SESSION',
'$_ENV', '$_COOKIE', '$http_response_header', '$argc', '$argv',
],
'unsafe_functions' => [
'now', 'time', 'date', 'env', 'getenv', 'cookie',
'request', 'session', 'dd', 'dump', 'var_dump',
'debug_backtrace', 'phpinfo', 'extract',
'get_defined_vars', 'parse_str', 'abort', 'abort_if',
'abort_unless',
],
],
];
9 changes: 9 additions & 0 deletions src/AbstractComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Illuminate\Support\Fluent;
use Illuminate\View\ComponentAttributeBag;
use Stillat\Dagger\Exceptions\RuntimeException;
use Stillat\Dagger\Parser\ComponentTap;
use Stillat\Dagger\Runtime\SlotContainer;

Expand Down Expand Up @@ -96,6 +97,14 @@ abstract public function trimOutput(): static;

abstract public function cache(): static;

/**
* @throws RuntimeException
*/
public function compiler(?bool $allowOptimizations = null): static
{
throw new RuntimeException('Cannot call compiler method at runtime.');
}

public function __get(string $name)
{
return $this->data->get($name);
Expand Down
30 changes: 30 additions & 0 deletions src/Compiler/ComponentState.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use InvalidArgumentException;
use Stillat\BladeParser\Nodes\Components\ComponentNode;
use Stillat\Dagger\Cache\CacheProperties;
use Stillat\Dagger\ComponentOptions;
use Stillat\Dagger\Support\Utils;

class ComponentState
Expand Down Expand Up @@ -63,17 +64,36 @@ class ComponentState

public string $validationMessages = '[]';

public ?bool $canCompileTimeRender = null;

public int $lineOffset = 0;

public ?Extractions $extractions = null;

public ComponentOptions $options;

/**
* Indicates if the component is eligible for compile-time rendering.
*
* We will keep it false by default, and only enable this if needed.
*/
public bool $isCtrEligible = false;

public ?CacheProperties $cacheProperties = null;

public function __construct(
public ?ComponentNode $node,
public string $varSuffix,
) {
$this->options = new ComponentOptions;
$this->updateNodeDetails($this->node, $this->varSuffix);
}

public function getDynamicVariables(): array
{
return $this->dynamicVariables;
}

/**
* @internal
*/
Expand Down Expand Up @@ -287,6 +307,11 @@ public function setPropDefaults(array $defaults): static
return $this;
}

public function getAllPropNames(): array
{
return array_merge($this->getPropNames(), $this->getAwareVariables());
}

public function getPropDefaults(): array
{
return $this->defaultPropValues;
Expand Down Expand Up @@ -342,6 +367,11 @@ public function setMixins(string $classes): static
return $this;
}

public function hasMixins(): bool
{
return $this->mixins != '';
}

public function getMixins(): string
{
return $this->mixins;
Expand Down
77 changes: 77 additions & 0 deletions src/Compiler/Concerns/ManagesComponentCtrState.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

namespace Stillat\Dagger\Compiler\Concerns;

use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\ParentConnectingVisitor;
use Stillat\Dagger\Compiler\Ctr\CompileTimeRendererVisitor;
use Stillat\Dagger\Parser\PhpParser;

trait ManagesComponentCtrState
{
protected ?CompileTimeRendererVisitor $ctrVisitor = null;

public function getCtrVisitor(): CompileTimeRendererVisitor
{
if ($this->ctrVisitor) {
return $this->ctrVisitor;
}

return $this->ctrVisitor = new CompileTimeRendererVisitor;
}

protected function containsOtherComponents(string $template): bool
{
preg_match_all(
'/<([a-zA-Z0-9_]+)([-:])[a-zA-Z0-9_\-:]*(?:\s[^>]*)?>/',
$template,
$matches,
PREG_SET_ORDER
);

if (! $matches) {
return false;
}

foreach ($matches as $match) {
if (! in_array(mb_strtolower($match[1]), $this->componentNamespaces)) {
return true;
}
}

return false;
}

protected function checkForCtrEligibility(string $originalTemplate, string $compiledTemplate): void
{
if (! $this->activeComponent->options->allowOptimizations) {
$this->activeComponent->isCtrEligible = false;

return;
}

if ($this->containsOtherComponents($originalTemplate)) {
$this->activeComponent->isCtrEligible = false;

return;
}

$traverser = new NodeTraverser;

$ast = PhpParser::makeParser()->parse($compiledTemplate);
$parentingVisitor = new ParentConnectingVisitor;
$traverser->addVisitor($parentingVisitor);
$traverser->traverse($ast);

$traverser->removeVisitor($parentingVisitor);

$ctrVisitor = $this->getCtrVisitor()
->reset()
->setComponentState($this->activeComponent);

$traverser->addVisitor($ctrVisitor);
$traverser->traverse($ast);

$this->activeComponent->isCtrEligible = $ctrVisitor->isEligibleForCtr();
}
}
Loading