Skip to content

Commit 314c97a

Browse files
authored
Merge pull request #8 from gwleuverink/feature/inject-core
Feature / Separate core & inject in full-page responses
2 parents 5d29542 + 0265769 commit 314c97a

File tree

15 files changed

+260
-87
lines changed

15 files changed

+260
-87
lines changed

config/bundle.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@
1919
|--------------------------------------------------------------------------
2020
|
2121
| The _import() function uses a built-in non blocking polling mechanism in
22-
| order to account for script tags that are not processed sequentially
23-
| and Alpine support. Here you can tweak it's internal timout in ms.
22+
| order to account for script tags that are not processed sequentially.
23+
| Here you can tweak it's internal timout in ms.
2424
|
2525
*/
26-
'import_resolution_timeout' => env('BUNDLE_IMPORT_RESOLUTION_TIMEOUT', 800),
26+
'import_resolution_timeout' => env('BUNDLE_IMPORT_RESOLUTION_TIMEOUT', 200),
2727

2828
/*
2929
|--------------------------------------------------------------------------

docs/introduction.md

+4-11
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ The <x-import /> component processes your import on the fly and renders a script
1717

1818
<!-- yields the following script -->
1919

20-
<script src="/x-import/e52def31336c.min.js" type="module" data-module="apexcharts" data-alias="ApexCharts"
21-
></script>
20+
<script src="/x-import/e52def31336c.min.js" type="module" data-module="apexcharts" data-alias="ApexCharts"></script>
2221
```
2322

2423
### A bit more in depth
@@ -33,19 +32,13 @@ Bun treats these bundles as being separate builds. This would cause collisions w
3332

3433
A script tag with `type="module"` also makes it `defer` by default, so they are loaded in parallel & executed in order.
3534

36-
When you use the `<x-import />` component Bundle constructs a small JS script that imports the desired module and exposes it on the page, along with the `_import` helper function. It then bundles it up and caches it in the `storage/app/bundle` directory. This is then either served over http or rendered inline.
37-
38-
<!--
39-
{: .note }
40-
> You may pass any attributes a script tag would accept, like `defer` or `async`. Note that scripts with `type="module"` are deferred by default.
41-
<br />
42-
-->
35+
When you use the `<x-import />` component Bundle constructs a small JS script that imports the desired module and exposes it on the page. It then bundles it up and caches it in the `storage/app/bundle` directory. This is then either served over http or rendered inline.
4336

4437
## The `_import` helper function
4538

46-
After you use `<x-import />` somewhere in your template a global `_import` function will become available on the window object.
39+
Bundle's core, which containst `_import` helper function and internal import map, is automatically injected on every page.
4740

48-
You can use this function to fetch the bundled import by the name you've passed to the `as` argument.
41+
The `_import` function may be used to fetch the bundled import by the name you've passed to the `as` argument.
4942

5043
```js
5144
var module = await _import("lodash"); // Resolves the module's default export

docs/roadmap.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,11 @@ It would be incredible if this object could be forwarded to Alpine directly like
102102
</div>
103103
```
104104

105-
## Injecting Bundle's core on every page
105+
## Injecting Bundle's core on every page
106106

107-
This will reduce every import's size slightly. And more importantly; it will remove the need to wrap `_import` calls inside script tags without `type="module"`, making things easier for the developer and greatly decrease the chance of unexpected behaviour caused by race conditions due to slow network speeds when a `DOMContentLoaded` listener was forgotten.
107+
**_Added in [v0.1.3](https://github.com/gwleuverink/bundle/releases/tag/v0.1.3)_**
108+
109+
This will reduce every import's size slightly. But more importantly; it will greatly decrease the chance of unexpected behaviour caused by race conditions, since the Bundle's core is available on pageload.
108110

109111
## Optionally assigning a import to the window scope
110112

src/Commands/Build.php

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Throwable;
66
use Illuminate\Console\Command;
7+
use Leuverink\Bundle\InjectCore;
78
use Symfony\Component\Finder\Finder;
89
use Illuminate\Support\Facades\Blade;
910
use Symfony\Component\Finder\SplFileInfo;
@@ -23,6 +24,9 @@ public function handle(Finder $finder): int
2324
{
2425
$this->callSilent('bundle:clear');
2526

27+
// Bundle the core
28+
InjectCore::new()->bundle();
29+
2630
// Find and bundle all components
2731
collect(config('bundle.build_paths'))
2832
// Find all files matching *.blade.*

src/Components/Import.php

-47
Original file line numberDiff line numberDiff line change
@@ -61,37 +61,12 @@ protected function raiseConsoleErrorOrException(BundlingFailedException $e)
6161
/** Builds Bundle's core JavaScript */
6262
protected function core(): string
6363
{
64-
$timeout = $this->manager()->config()->get('import_resolution_timeout');
65-
6664
return <<< JS
6765
//--------------------------------------------------------------------------
6866
// Expose x_import_modules map
6967
//--------------------------------------------------------------------------
7068
if(!window.x_import_modules) window.x_import_modules = {};
7169
72-
//--------------------------------------------------------------------------
73-
// Expose _import function (as soon as possible)
74-
//--------------------------------------------------------------------------
75-
window._import = async function(alias, exportName = 'default') {
76-
77-
// Wait for module to become available (Needed for Alpine support)
78-
const module = await poll(
79-
() => window.x_import_modules[alias],
80-
{$timeout}, 5, alias
81-
)
82-
83-
if(module === undefined) {
84-
console.info('When invoking _import() from a script tag make sure it has type="module"')
85-
throw `BUNDLE ERROR: '\${alias}' not found`;
86-
}
87-
88-
return module[exportName] !== undefined
89-
// Return export if it exists
90-
? module[exportName]
91-
// Otherwise the entire module
92-
: module
93-
};
94-
9570
//--------------------------------------------------------------------------
9671
// Import the module & push to x_import_modules
9772
// Invoke IIFE so we can break out of execution when needed
@@ -117,28 +92,6 @@ protected function core(): string
11792
: import('{$this->module}')
11893
})();
11994
120-
121-
//--------------------------------------------------------------------------
122-
// Non-blocking polling mechanism
123-
//--------------------------------------------------------------------------
124-
async function poll(success, timeout, interval, ref) {
125-
const startTime = new Date().getTime();
126-
127-
while (true) {
128-
// If the success callable returns something truthy, return
129-
let result = success()
130-
if (result) return result;
131-
132-
// Check if timeout has elapsed
133-
const elapsedTime = new Date().getTime() - startTime;
134-
if (elapsedTime >= timeout) {
135-
throw `BUNDLE TIMEOUT: '\${ref}' could not be resolved`;
136-
}
137-
138-
// Wait for a set interval
139-
await new Promise(resolve => setTimeout(resolve, interval));
140-
}
141-
};
14295
JS;
14396
}
14497
}

src/Components/views/script.blade.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
@once("bundle:$module:$as")
33
<!--[BUNDLE: {{ $as }} from '{{ $module }}']-->
44
<?php if($inline) { ?>
5-
<script data-module="{{ $module }}" data-alias="{{ $as }}" type="module" {{ $attributes }}>
5+
<script data-module="{{ $module }}" data-alias="{{ $as }}" type="module">
66
{!! file_get_contents($bundle) !!}
77
</script>
88
<?php } else { ?>
9-
<script src="{{ route('bundle:import', $bundle->getFilename(), false) }}" data-module="{{ $module }}" data-alias="{{ $as }}" type="module" {{ $attributes }}></script>
9+
<script src="{{ route('bundle:import', $bundle->getFilename(), false) }}" data-module="{{ $module }}" data-alias="{{ $as }}" type="module"></script>
1010
<?php } ?>
1111
<!--[ENDBUNDLE]>-->
1212
@else {{-- @once else clause --}}

src/InjectCore.php

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<?php
2+
3+
namespace Leuverink\Bundle;
4+
5+
use SplFileInfo;
6+
use Leuverink\Bundle\Traits\Constructable;
7+
use Illuminate\Foundation\Http\Events\RequestHandled;
8+
use Leuverink\Bundle\Contracts\BundleManager as BundleManagerContract;
9+
10+
class InjectCore
11+
{
12+
use Constructable;
13+
14+
/** Injects a inline script tag containing Bundle's core inside every full-page response */
15+
public function __invoke(RequestHandled $handled)
16+
{
17+
$html = $handled->response->getContent();
18+
19+
// Skip if request doesn't return a full page
20+
if (! str_contains($html, '</html>')) {
21+
return;
22+
}
23+
24+
// Skip if core was included before
25+
if (str_contains($html, '<!--[BUNDLE-CORE]-->')) {
26+
return;
27+
}
28+
29+
// Bundle it up & wrap in script tag
30+
$script = $this->wrapInScriptTag(
31+
file_get_contents($this->bundle())
32+
);
33+
34+
// Inject into response
35+
$originalContent = $handled->response->original;
36+
37+
$handled->response->setContent(
38+
$this->injectAssets($html, $script)
39+
);
40+
41+
$handled->response->original = $originalContent;
42+
}
43+
44+
public function bundle(): SplFileInfo
45+
{
46+
return $this->manager()->bundle(
47+
$this->core()
48+
);
49+
}
50+
51+
/** Get an instance of the BundleManager */
52+
protected function manager(): BundleManagerContract
53+
{
54+
return BundleManager::new();
55+
}
56+
57+
/** Injects Bundle's core into given html string (taken from Livewire's injection mechanism) */
58+
protected function injectAssets(string $html, string $core): string
59+
{
60+
$html = str($html);
61+
62+
if ($html->test('/<\s*\/\s*head\s*>/i')) {
63+
return $html
64+
->replaceMatches('/(<\s*\/\s*head\s*>)/i', $core . '$1')
65+
->toString();
66+
}
67+
68+
return $html
69+
->replaceMatches('/(<\s*html(?:\s[^>])*>)/i', '$1' . $core)
70+
->toString();
71+
}
72+
73+
/** Wrap the contents in a inline script tag */
74+
protected function wrapInScriptTag($contents): string
75+
{
76+
return <<< HTML
77+
<!--[BUNDLE-CORE]-->
78+
<script type="module" data-bundle="core">
79+
{$contents}
80+
</script>
81+
<!--[ENDBUNDLE]>-->
82+
HTML;
83+
}
84+
85+
protected function core(): string
86+
{
87+
$timeout = $this->manager()->config()->get('import_resolution_timeout');
88+
89+
return <<< JS
90+
91+
//--------------------------------------------------------------------------
92+
// Expose x_import_modules map
93+
//--------------------------------------------------------------------------
94+
if(!window.x_import_modules) window.x_import_modules = {};
95+
96+
97+
//--------------------------------------------------------------------------
98+
// Expose _import function
99+
//--------------------------------------------------------------------------
100+
window._import = async function(alias, exportName = 'default') {
101+
102+
// Wait for module to become available (account for invoking from non-deferred script)
103+
const module = await poll(
104+
() => window.x_import_modules[alias],
105+
{$timeout}, 5, alias
106+
)
107+
108+
if(module === undefined) {
109+
console.info('When invoking _import() from a script tag make sure it has type="module"')
110+
throw `BUNDLE ERROR: '\${alias}' not found`;
111+
}
112+
113+
return module[exportName] !== undefined
114+
// Return export if it exists
115+
? module[exportName]
116+
// Otherwise the entire module
117+
: module
118+
};
119+
120+
121+
//--------------------------------------------------------------------------
122+
// Non-blocking polling mechanism
123+
//--------------------------------------------------------------------------
124+
async function poll(success, timeout, interval, ref) {
125+
const startTime = new Date().getTime();
126+
127+
while (true) {
128+
// If the success callable returns something truthy, return
129+
let result = success()
130+
if (result) return result;
131+
132+
// Check if timeout has elapsed
133+
const elapsedTime = new Date().getTime() - startTime;
134+
if (elapsedTime >= timeout) {
135+
throw `BUNDLE TIMEOUT: '\${ref}' could not be resolved`;
136+
}
137+
138+
// Wait for a set interval
139+
await new Promise(resolve => setTimeout(resolve, interval));
140+
}
141+
};
142+
143+
JS;
144+
}
145+
}

src/ServiceProvider.php

+11
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
use Leuverink\Bundle\Commands\Build;
99
use Leuverink\Bundle\Commands\Clear;
1010
use Illuminate\Support\Facades\Blade;
11+
use Illuminate\Support\Facades\Event;
1112
use Illuminate\Support\Facades\Route;
1213
use Leuverink\Bundle\Components\Import;
14+
use Illuminate\Foundation\Http\Events\RequestHandled;
1315
use Illuminate\Support\ServiceProvider as BaseServiceProvider;
1416
use Leuverink\Bundle\Contracts\BundleManager as BundleManagerContract;
1517

@@ -21,6 +23,7 @@ public function boot(): void
2123

2224
$this->registerComponents();
2325
$this->registerCommands();
26+
$this->injectCore();
2427
}
2528

2629
public function register()
@@ -51,6 +54,14 @@ protected function registerComponents()
5154
Blade::component('import', Import::class);
5255
}
5356

57+
protected function injectCore()
58+
{
59+
Event::listen(
60+
RequestHandled::class,
61+
InjectCore::class,
62+
);
63+
}
64+
5465
protected function registerCommands()
5566
{
5667
$this->commands(Build::class);

tests/Browser/InjectsCoreTest.php

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace Leuverink\Bundle\Tests\Browser;
4+
5+
use Leuverink\Bundle\Tests\DuskTestCase;
6+
7+
// Pest & Workbench Dusk don't play nicely together
8+
// We need to fall back to PHPUnit syntax.
9+
10+
class InjectsCoreTest extends DuskTestCase
11+
{
12+
/** @test */
13+
public function it_injects_import_and_import_function_on_the_window_object_without_using_the_import_component()
14+
{
15+
$this->blade('')
16+
->assertScript('typeof window._import', 'function')
17+
->assertScript('typeof window.x_import_modules', 'object');
18+
}
19+
}

0 commit comments

Comments
 (0)