Skip to content
This repository has been archived by the owner on Jul 21, 2023. It is now read-only.

Commit

Permalink
added brotli compression support (#62)
Browse files Browse the repository at this point in the history
  • Loading branch information
pjordaan authored and nicoschoenmaker committed Jan 29, 2018
1 parent e6885d4 commit d62b0ff
Show file tree
Hide file tree
Showing 9 changed files with 223 additions and 22 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"url": "https://github.com/hostnet/resolver-lib/issues"
},
"dependencies": {
"brotli": "1.3.2",
"clean-css": "^4.1.9",
"less": "^2.7.0",
"typescript": "^2.5.0",
Expand Down
6 changes: 6 additions & 0 deletions spec/processor.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ describe("processor", function () {
expect(processor.LES).toEqual("LES");
expect(processor.UGL).toEqual("UGL");
expect(processor.CLE).toEqual("CLE");
expect(processor.BRO).toEqual("BRO");
});

it("process unknown", function () {
Expand All @@ -21,6 +22,11 @@ describe("processor", function () {
expect(processor.process(processor.LES, 'a.css', ".bla { color: red; }")).toEqual(expected);
});

it("process brotli", function () {
var expected = ".bla {\n color: red;\n}\n";
expect(processor.process(processor.BRO, __filename, '')).toEqual(jasmine.any(Uint8Array));
});

it("process less error", function () {
try {
processor.process(processor.LES, 'a.css', ".bla {");
Expand Down
5 changes: 5 additions & 0 deletions src/Bundler/Runner/RunnerType.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ final class RunnerType
*/
public const CLEAN_CSS = 'CLE';

/**
* @see https://github.com/devongovett/brotli.js
*/
public const BROTLI = 'BRO';

/**
* @codeCoverageIgnore private by design because this is an ENUM class
*/
Expand Down
59 changes: 59 additions & 0 deletions src/EventListener/BrotliListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php
/**
* @copyright 2017 Hostnet B.V.
*/
declare(strict_types=1);
namespace Hostnet\Component\Resolver\EventListener;

use Hostnet\Component\Resolver\Bundler\ContentItem;
use Hostnet\Component\Resolver\Bundler\Runner\RunnerInterface;
use Hostnet\Component\Resolver\Bundler\Runner\RunnerType;
use Hostnet\Component\Resolver\Event\FileEvent;
use Hostnet\Component\Resolver\File;
use Hostnet\Component\Resolver\FileSystem\FileWriter;
use Hostnet\Component\Resolver\FileSystem\StringReader;
use Symfony\Component\EventDispatcher\EventDispatcher;

/**
* The brotli listener will output another file with a .br extension contain a brotli compressed asset.
*
* @see https://github.com/devongovett/brotli.js
*/
class BrotliListener
{
private $runner;

private $dispatcher;

private $project_root;

public function __construct(RunnerInterface $runner, EventDispatcher $dispatcher, string $project_root)
{
$this->runner = $runner;
$this->dispatcher = $dispatcher;
$this->project_root = $project_root;
}

/**
* @param FileEvent $event
*/
public function onPostWrite(FileEvent $event): void
{
$file = $event->getFile();
// if the file is already compressed with brotli/gzip, do not compress it again as we do not serve files
// like .br.gz.br
if (preg_match('/\.(gz|br)$/', $file->path)) {
return;
}
$content = $event->getContent();
$item = new ContentItem($file, $file->getName(), new StringReader($content));
$brotli_contents = $this->runner->execute(RunnerType::BROTLI, $item);
// the runner returns an empty string if it could not be brotli compressed. This seems to be the
// case for some of the binary files. Maybe we should blacklist binary files, but in general
// any file could benefit from brotli compression.
if (!empty($brotli_contents) && strlen($brotli_contents) < strlen($content)) {
$writer = new FileWriter($this->dispatcher, $this->project_root);
$writer->write(new File($file->path . '.br'), $brotli_contents);
}
}
}
26 changes: 26 additions & 0 deletions src/Plugin/BrotliPlugin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php
/**
* @copyright 2017 Hostnet B.V.
*/
declare(strict_types=1);
namespace Hostnet\Component\Resolver\Plugin;

use Hostnet\Component\Resolver\Event\FileEvents;
use Hostnet\Component\Resolver\EventListener\BrotliListener;

/**
* This plugin outputs a brotli-compressed file next to the output file.
* Enabling this with the correct web server settings will serve a static brotli compressed file if the browser
* accepts brotli encoding. Brotli is superior in compression size compared to gzip, but it is slower to dynamically
* compress it, so a pre-compressed asset is recommended.
*/
class BrotliPlugin implements PluginInterface
{
public function activate(PluginApi $plugin_api): void
{
$config = $plugin_api->getConfig();
$dispatcher = $config->getEventDispatcher();
$brotli_listener = new BrotliListener($plugin_api->getRunner(), $dispatcher, $config->getProjectRoot());
$dispatcher->addListener(FileEvents::POST_WRITE, [$brotli_listener, 'onPostWrite']);
}
}
8 changes: 7 additions & 1 deletion src/Plugin/GzipPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@ public function activate(PluginApi $plugin_api): void
$config = $plugin_api->getConfig();
$dispatcher = $config->getEventDispatcher();
$dispatcher->addListener(FileEvents::POST_WRITE, function (FileEvent $ev) use ($config, $dispatcher) {
$content = $ev->getContent();
$file = $ev->getFile();
$content = $ev->getContent();
// if the file is already compressed with brotli/gzip, do not compress it again as we do not serve files
// like .br.gz.br
if (preg_match('/\.(gz|br)$/', $file->path)) {
return;
}
$gzip_content = gzencode($content, 9);
if (strlen($gzip_content) < strlen($content)) {
$writer = new FileWriter($dispatcher, $config->getProjectRoot());
Expand Down
49 changes: 28 additions & 21 deletions src/Resources/processor.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,30 @@
var CleanCSS = require("clean-css");
var less = require("less");
var path = require('path');
var ts = require("typescript");
var UglifyJS = require("uglify-js");

(function () {
var compilers = {
TSC: function (filename, source) {
var result = ts.transpileModule(source, {
compilerOptions: {
inlineSourceMap: false,
skipLibCheck: true,
importHelpers: true,
target: ts.ScriptTarget.ES5,
module: ts.ModuleKind.CommonJS,
moduleResolution: ts.ModuleResolutionKind.NodeJs,
emitDecoratorMetadata: true,
experimentalDecorators: true
}
});
var ts = require("typescript"),
result = ts.transpileModule(source, {
compilerOptions: {
inlineSourceMap: false,
skipLibCheck: true,
importHelpers: true,
target: ts.ScriptTarget.ES5,
module: ts.ModuleKind.CommonJS,
moduleResolution: ts.ModuleResolutionKind.NodeJs,
emitDecoratorMetadata: true,
experimentalDecorators: true
}
});

return result.outputText;
},
LES: function (filename, source) {
var css = null;
var less = require("less"),
css = null;
less.render(source, {
"filename": path.resolve(filename),
"syncImport": true
filename: path.resolve(filename),
syncImport: true
}, function (error, output) {
if (null !== error) {
throw error.message + ' in ' + error.filename + ' on line ' + error.line;
Expand All @@ -36,17 +34,25 @@ var UglifyJS = require("uglify-js");
return css;
},
UGL: function (filename, source) {
var result = UglifyJS.minify(source);
var UglifyJS = require("uglify-js"),
result = UglifyJS.minify(source);
if (result.error) {
throw result.error;
}

return result.code;
},
CLE: function (filename, source) {
var options = {/* options */},
var CleanCSS = require("clean-css"),
options = {/* options */},
output = new CleanCSS(options).minify(source);
return output.styles;
},
BRO: function (filename, source) {
var compress = require('brotli/compress'),
fs = require('fs'),
options = { mode: 0, quality: 11, lgwin: 22 };
return compress(fs.readFileSync(filename), options) || '';
}
};

Expand All @@ -55,6 +61,7 @@ var UglifyJS = require("uglify-js");
LES: 'LES', // Less
UGL: 'UGL', // Uglify
CLE: 'CLE', // Clean CSS
BRO: 'BRO', // Brotli compression
process: function (type, filename, message) {
if (compilers.hasOwnProperty(type)) {
return compilers[type](filename, message);
Expand Down
52 changes: 52 additions & 0 deletions test/EventListener/BrotliListenerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php
/**
* @copyright 2018 Hostnet B.V.
*/
declare(strict_types=1);
namespace Hostnet\Component\Resolver\EventListener;

use Hostnet\Component\Resolver\Bundler\ContentItem;
use Hostnet\Component\Resolver\Bundler\Runner\RunnerInterface;
use Hostnet\Component\Resolver\Bundler\Runner\RunnerType;
use Hostnet\Component\Resolver\Event\FileEvent;
use Hostnet\Component\Resolver\File;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Symfony\Component\EventDispatcher\EventDispatcher;

/**
* @covers \Hostnet\Component\Resolver\EventListener\BrotliListener
*/
class BrotliListenerTest extends TestCase
{
public function testOnPostWrite()
{
$file = new File(__FILE__);
$contents = file_get_contents(__FILE__);

$runner = $this->prophesize(RunnerInterface::class);
$runner->execute(
RunnerType::BROTLI,
Argument::that(function (ContentItem $item) use ($contents, $file) {
self::assertEquals($contents, $item->getContent());
self::assertEquals($file, $item->file);
return true;
})
)->willReturn('brotli.js');
$dispatcher = new EventDispatcher();
$project_root = __DIR__;

$brotli_listener = new BrotliListener(
$runner->reveal(),
$dispatcher,
$project_root
);
@unlink(__FILE__ . '.br');
try {
$brotli_listener->onPostWrite(new FileEvent($file, $contents));
self::assertTrue(file_exists(__FILE__ . '.br'));
} finally {
@unlink(__FILE__ . '.br');
}
}
}
39 changes: 39 additions & 0 deletions test/Plugin/BrotliPluginTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php
/**
* @copyright 2018 Hostnet B.V.
*/
declare(strict_types=1);
namespace Hostnet\Component\Resolver\Plugin;

use Hostnet\Component\Resolver\Bundler\Runner\RunnerInterface;
use Hostnet\Component\Resolver\Config\ConfigInterface;
use Hostnet\Component\Resolver\Event\FileEvents;
use PHPUnit\Framework\TestCase;
use Symfony\Component\EventDispatcher\EventDispatcher;

/**
* @covers \Hostnet\Component\Resolver\Plugin\BrotliPlugin
*/
class BrotliPluginTest extends TestCase
{
public function testActivate()
{
$brotli_plugin = new BrotliPlugin();

$event_dispatcher = new EventDispatcher();

$config = $this->prophesize(ConfigInterface::class);
$config->getEventDispatcher()->willReturn($event_dispatcher);
$config->getProjectRoot()->willReturn(__DIR__);

$runner = $this->prophesize(RunnerInterface::class);

$plugin_api = $this->prophesize(PluginApi::class);
$plugin_api->getRunner()->wilLReturn($runner->reveal());
$plugin_api->getConfig()->willReturn($config->reveal());

self::assertFalse($event_dispatcher->hasListeners(FileEvents::POST_WRITE));
$brotli_plugin->activate($plugin_api->reveal());
self::assertTrue($event_dispatcher->hasListeners(FileEvents::POST_WRITE));
}
}

0 comments on commit d62b0ff

Please sign in to comment.