├── .gitignore ├── tests ├── Basset │ ├── Manifest │ │ ├── fixtures │ │ │ └── collections.json │ │ ├── ManifestTest.php │ │ └── EntryTest.php │ ├── Filter │ │ ├── UriRewriteFilterTest.php │ │ ├── CssoFilterTest.php │ │ └── FilterTest.php │ ├── Factory │ │ ├── FilterFactoryTest.php │ │ └── AssetFactoryTest.php │ ├── EnvironmentTest.php │ ├── Builder │ │ ├── FilesystemCleanerTest.php │ │ └── BuilderTest.php │ ├── CollectionTest.php │ ├── AssetFinderTest.php │ ├── AssetTest.php │ ├── ServerTest.php │ └── DirectoryTest.php └── Cases │ └── FilterTestCase.php ├── src ├── Basset │ ├── Exceptions │ │ ├── AssetNotFoundException.php │ │ ├── BuildNotRequiredException.php │ │ └── DirectoryNotFoundException.php │ ├── Facade.php │ ├── Factory │ │ ├── Factory.php │ │ ├── FactoryManager.php │ │ ├── FilterFactory.php │ │ └── AssetFactory.php │ ├── Filter │ │ ├── CssoFilter.php │ │ ├── Filterable.php │ │ └── UriRewriteFilter.php │ ├── Console │ │ ├── BassetCommand.php │ │ └── BuildCommand.php │ ├── Manifest │ │ ├── Manifest.php │ │ └── Entry.php │ ├── Collection.php │ ├── Environment.php │ ├── Builder │ │ ├── FilesystemCleaner.php │ │ └── Builder.php │ ├── AssetFinder.php │ ├── BassetServiceProvider.php │ ├── Server.php │ ├── Asset.php │ └── Directory.php ├── helpers.php └── config │ └── config.php ├── .travis.yml ├── phpunit.xml ├── composer.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /coverage 3 | composer.phar 4 | composer.lock 5 | .DS_Store 6 | *.sublime-* -------------------------------------------------------------------------------- /tests/Basset/Manifest/fixtures/collections.json: -------------------------------------------------------------------------------- 1 | {"foo":{"fingerprints":{"stylesheets":"bar"},"development":{"stylesheets":{"baz/qux.scss":"baz/qux.css"}}}} -------------------------------------------------------------------------------- /src/Basset/Exceptions/AssetNotFoundException.php: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/Cases/FilterTestCase.php: -------------------------------------------------------------------------------- 1 | find($name); 26 | } 27 | 28 | 29 | } -------------------------------------------------------------------------------- /src/Basset/Factory/Factory.php: -------------------------------------------------------------------------------- 1 | log = $log; 30 | 31 | return $this; 32 | } 33 | 34 | /** 35 | * Set the factory manager instance. 36 | * 37 | * @param \Basset\Factory\FactoryManager $factory 38 | * @return \Basset\Factory\Factory 39 | */ 40 | public function setFactoryManager(FactoryManager $factory) 41 | { 42 | $this->factory = $factory; 43 | 44 | return $this; 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /tests/Basset/Filter/UriRewriteFilterTest.php: -------------------------------------------------------------------------------- 1 | load(); 17 | 18 | $filter->filterDump($asset); 19 | 20 | $this->assertEquals("body { background-image: url('/foo/bar.png'); }", $asset->getContent()); 21 | } 22 | 23 | 24 | public function testUriRewriteWithSymlinks() 25 | { 26 | $filter = new UriRewriteFilter('path/to/public', array('//assets' => strtr('path/to/outside/public/assets', '/', DIRECTORY_SEPARATOR))); 27 | 28 | $input = "body { background-image: url('../foo/bar.png'); }"; 29 | 30 | $asset = new StringAsset($input, array(), 'path/to/outside/public/assets/baz', 'qux.css'); 31 | $asset->load(); 32 | 33 | $filter->filterDump($asset); 34 | 35 | $this->assertEquals("body { background-image: url('/assets/foo/bar.png'); }", $asset->getContent()); 36 | } 37 | 38 | 39 | } -------------------------------------------------------------------------------- /tests/Basset/Filter/CssoFilterTest.php: -------------------------------------------------------------------------------- 1 | findExecutable('csso', 'CSSO_BIN'); 16 | $nodeBin = $this->findExecutable('node', 'NODE_BIN'); 17 | 18 | if ( ! $cssoBin or ! $nodeBin) 19 | { 20 | $this->markTestIncomplete('Could not find CSSO or Node executables.'); 21 | } 22 | 23 | $this->filter = new CssoFilter($cssoBin, $nodeBin); 24 | } 25 | 26 | 27 | public function tearDown() 28 | { 29 | $this->filter = null; 30 | } 31 | 32 | 33 | public function testCsso() 34 | { 35 | $input = '.test { height: 10px; height: 20px; }'; 36 | 37 | $asset = new StringAsset($input); 38 | $asset->load(); 39 | 40 | try 41 | { 42 | $this->filter->filterLoad($asset); 43 | } 44 | catch (FilterException $e) 45 | { 46 | $this->markTestIncomplete('Could not properly test CSSO filter. Make sure Node and CSSO are in your PATH.'); 47 | } 48 | 49 | $this->assertEquals('.test{height:20px}', $asset->getContent()); 50 | } 51 | 52 | 53 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jasonlewis/basset", 3 | "description": "A better asset management package for Laravel.", 4 | "keywords": ["assets", "basset", "laravel"], 5 | "license": "BSD-2-Clause", 6 | "authors": [ 7 | { 8 | "name": "Jason Lewis", 9 | "email": "jason.lewis1991@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=5.3.0", 14 | "kriswallsmith/assetic": "1.1.*" 15 | }, 16 | "require-dev": { 17 | "mockery/mockery": ">=0.7.2", 18 | "illuminate/config": "4.0.*", 19 | "illuminate/console": "4.0.*", 20 | "illuminate/filesystem": "4.0.*", 21 | "illuminate/log": "4.0.*", 22 | "illuminate/routing": "4.0.*", 23 | "illuminate/support": "4.0.*", 24 | "symfony/process": "2.3.*" 25 | }, 26 | "suggest": { 27 | "aws/aws-sdk-php": "Deploy static assets directly to your S3 buckets.", 28 | "rackspace/php-cloudfiles": "Deploy static assets directly to your Cloud Files container." 29 | }, 30 | "autoload": { 31 | "psr-0": { 32 | "Basset": "src/" 33 | }, 34 | "classmap": [ 35 | "tests/Cases/FilterTestCase.php" 36 | ], 37 | "files": ["src/helpers.php"] 38 | }, 39 | "extra": { 40 | "branch-alias": { 41 | "dev-master": "4.0-dev" 42 | } 43 | }, 44 | "minimum-stability": "dev" 45 | } -------------------------------------------------------------------------------- /tests/Basset/Factory/FilterFactoryTest.php: -------------------------------------------------------------------------------- 1 | factory = new Basset\Factory\FilterFactory(array('foo' => 'FooFilter', 'bar' => array('BarFilter', function($filter) 17 | { 18 | $filter->setArgument('foo'); 19 | })), array(), 'testing'); 20 | 21 | $this->factory->setLogger(m::mock('Illuminate\Log\Writer')); 22 | } 23 | 24 | 25 | public function testMakeNewFilterInstanceFromString() 26 | { 27 | $this->assertInstanceOf('Basset\Filter\Filter', $this->factory->make('FooFilter')); 28 | } 29 | 30 | 31 | public function testMakeFilterInstanceFromExistingFilterInstance() 32 | { 33 | $filter = m::mock('Basset\Filter\Filter'); 34 | $this->assertEquals($filter, $this->factory->make($filter)); 35 | } 36 | 37 | 38 | public function testMakeFromConfigAlias() 39 | { 40 | $filter = $this->factory->make('foo'); 41 | $this->assertEquals('FooFilter', $filter->getFilter()); 42 | } 43 | 44 | 45 | public function testMakeFromConfigAliasWithCallback() 46 | { 47 | $filter = $this->factory->make('bar'); 48 | $this->assertContains('foo', $filter->getArguments()); 49 | } 50 | 51 | 52 | } -------------------------------------------------------------------------------- /src/Basset/Factory/FactoryManager.php: -------------------------------------------------------------------------------- 1 | driver($factory); 16 | } 17 | 18 | /** 19 | * Create the asset factory driver. 20 | * 21 | * @return \Basset\Factory\AssetFactory 22 | */ 23 | public function createAssetDriver() 24 | { 25 | $asset = new AssetFactory($this->app['files'], $this->app['env'], $this->app['path.public']); 26 | 27 | return $this->factory($asset); 28 | } 29 | 30 | /** 31 | * Create the filter factory driver. 32 | * 33 | * @return \Basset\Factory\FilterFactory 34 | */ 35 | public function createFilterDriver() 36 | { 37 | $aliases = $this->app['config']->get('basset::aliases.filters', array()); 38 | 39 | $node = $this->app['config']->get('basset::node_paths', array()); 40 | 41 | $filter = new FilterFactory($aliases, $node, $this->app['env']); 42 | 43 | return $this->factory($filter); 44 | } 45 | 46 | /** 47 | * Set the logger and factory manager on the factory instance. 48 | * 49 | * @param \Basset\Factory\Factory $factory 50 | * @return \Basset\Factory\Factory 51 | */ 52 | protected function factory(Factory $factory) 53 | { 54 | $factory->setLogger($this->getLogger()); 55 | 56 | return $factory->setFactoryManager($this); 57 | } 58 | 59 | /** 60 | * Get the log writer instance. 61 | * 62 | * @return \Illuminate\Log\Writer 63 | */ 64 | public function getLogger() 65 | { 66 | return $this->app['basset.log']; 67 | } 68 | 69 | } -------------------------------------------------------------------------------- /tests/Basset/Factory/AssetFactoryTest.php: -------------------------------------------------------------------------------- 1 | files = m::mock('Illuminate\Filesystem\Filesystem'); 17 | $this->asset = new Basset\Factory\AssetFactory($this->files, 'testing', __DIR__); 18 | 19 | $this->asset->setLogger($this->log = m::mock('Illuminate\Log\Writer')); 20 | $this->asset->setFactoryManager(m::mock('Basset\Factory\FactoryManager')); 21 | } 22 | 23 | 24 | public function testMakeAsset() 25 | { 26 | $asset = $this->asset->make(__FILE__); 27 | 28 | $this->assertEquals(basename(__FILE__), $asset->getRelativePath()); 29 | $this->assertEquals(__FILE__, $asset->getAbsolutePath()); 30 | } 31 | 32 | 33 | public function testBuildingOfAbsolutePath() 34 | { 35 | $this->assertEquals(__FILE__, $this->asset->buildAbsolutePath(__FILE__)); 36 | $this->assertEquals('http://foo.com', $this->asset->buildAbsolutePath('http://foo.com')); 37 | $this->assertEquals('//foo.com', $this->asset->buildAbsolutePath('//foo.com')); 38 | } 39 | 40 | public function testBuildingOfRelativePath() 41 | { 42 | $this->assertEquals('foo.css', $this->asset->buildRelativePath(__DIR__.'/foo.css')); 43 | $this->assertEquals('bar/foo.css', $this->asset->buildRelativePath(__DIR__.'/bar/foo.css')); 44 | $this->assertEquals('http://foo.com', $this->asset->buildRelativePath('http://foo.com')); 45 | } 46 | 47 | 48 | public function testBuildingOfRelativePathFromOutsidePublicDirectory() 49 | { 50 | $this->assertEquals(md5('path/to/outside').'/foo.css', $this->asset->buildRelativePath('path/to/outside/foo.css')); 51 | $this->assertEquals(md5('path/to').'/bar.css', $this->asset->buildRelativePath('path/to/bar.css')); 52 | } 53 | 54 | 55 | } -------------------------------------------------------------------------------- /src/Basset/Factory/FilterFactory.php: -------------------------------------------------------------------------------- 1 | aliases = $aliases; 40 | $this->nodePaths = $nodePaths; 41 | $this->appEnvironment = $appEnvironment; 42 | } 43 | 44 | /** 45 | * Make a new filter instance. 46 | * 47 | * @param string|\Basset\Filter\Filter $filter 48 | * @return \Basset\Filter\Filter 49 | */ 50 | public function make($filter) 51 | { 52 | if ($filter instanceof Filter) 53 | { 54 | return $filter; 55 | } 56 | 57 | $filter = isset($this->aliases[$filter]) ? $this->aliases[$filter] : $filter; 58 | 59 | if (is_array($filter)) 60 | { 61 | list($filter, $callback) = array(current($filter), next($filter)); 62 | } 63 | 64 | // If the filter was aliased and the value of the array was a callable closure then 65 | // we'll return and fire the callback on the filter instance so that any arguments 66 | // can be set for the filters constructor. 67 | $filter = new Filter($this->log, $filter, $this->nodePaths, $this->appEnvironment); 68 | 69 | if (isset($callback) and is_callable($callback)) 70 | { 71 | call_user_func($callback, $filter); 72 | } 73 | 74 | return $filter; 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /tests/Basset/EnvironmentTest.php: -------------------------------------------------------------------------------- 1 | finder = m::mock('Basset\AssetFinder'); 18 | $this->finder->shouldReceive('setWorkingDirectory')->with('/')->andReturn('/'); 19 | 20 | $this->environment = new Environment(m::mock('Basset\Factory\FactoryManager'), $this->finder); 21 | } 22 | 23 | 24 | public function testMakingNewCollectionReturnsNewCollectionInstance() 25 | { 26 | $this->assertInstanceOf('Basset\Collection', $this->environment->collection('foo')); 27 | } 28 | 29 | 30 | public function testMakingNewCollectionFiresCallback() 31 | { 32 | $fired = false; 33 | 34 | $this->environment->collection('foo', function() use (&$fired) { $fired = true; }); 35 | $this->assertTrue($fired); 36 | } 37 | 38 | 39 | public function testRegisterPackageNamespaceAndVendorWithEnvironmentAndFinder() 40 | { 41 | $this->finder->shouldReceive('addNamespace')->once()->with('bar', 'foo/bar'); 42 | $this->environment->package('foo/bar', 'bar'); 43 | } 44 | 45 | 46 | public function testRegisterPackageNamespaceAndVendorWithEnvironmentAndFinderAndGuessNamespace() 47 | { 48 | $this->finder->shouldReceive('addNamespace')->once()->with('bar', 'foo/bar'); 49 | $this->environment->package('foo/bar'); 50 | } 51 | 52 | 53 | public function testRegisteringArrayOfCollections() 54 | { 55 | $this->environment->collections(array( 56 | 'foo' => function(){}, 57 | 'bar' => function(){} 58 | )); 59 | $this->assertCount(2, $this->environment->all()); 60 | $this->assertArrayHasKey('foo', $this->environment->all()); 61 | } 62 | 63 | 64 | public function testCheckingIfEnvironmentHasCollection() 65 | { 66 | $this->assertFalse($this->environment->has('foo')); 67 | $this->environment->collection('foo'); 68 | $this->assertTrue($this->environment->has('foo')); 69 | } 70 | 71 | 72 | } -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 | all() as $identifier => $collection) $args[] = array("{$identifier}.css", "{$identifier}.js"); 53 | } 54 | 55 | array_walk_recursive($args, function($v, $k) use (&$collections) { is_numeric($k) ? ($collections[$v] = null) : ($collections[$k] = $v); }); 56 | 57 | foreach ($collections as $collection => $format) $responses[] = app('basset.server')->collection($collection, $format); 58 | 59 | return array_to_newlines($responses); 60 | } 61 | } 62 | 63 | if ( ! function_exists('array_to_newlines')) 64 | { 65 | /** 66 | * Convert an array to a newline separated string. 67 | * 68 | * @param array $array 69 | * @return string 70 | */ 71 | function array_to_newlines(array $array) 72 | { 73 | return implode(PHP_EOL, $array); 74 | } 75 | } -------------------------------------------------------------------------------- /src/Basset/Filter/CssoFilter.php: -------------------------------------------------------------------------------- 1 | cssoBin = $cssoBin; 33 | $this->nodeBin = $nodeBin; 34 | } 35 | 36 | /** 37 | * Apply filter on file load. 38 | * 39 | * @param \Assetic\Asset\AssetInterface $asset 40 | * @return void 41 | */ 42 | public function filterLoad(AssetInterface $asset) 43 | { 44 | $inputFile = tempnam(sys_get_temp_dir(), 'csso'); 45 | 46 | file_put_contents($inputFile, $asset->getContent()); 47 | 48 | // Before we create our process builder we'll create the arguments to be given to the builder. 49 | // If we have a node bin supplied then we'll shift that to the beginning of the array. 50 | $builderArguments = array($this->cssoBin); 51 | 52 | if ( ! is_null($this->nodeBin)) 53 | { 54 | array_unshift($builderArguments, $this->nodeBin); 55 | } 56 | 57 | $builder = $this->createProcessBuilder($builderArguments); 58 | 59 | $builder->add($inputFile); 60 | 61 | // Get the process from the builder and run the process. 62 | $process = $builder->getProcess(); 63 | 64 | $code = $process->run(); 65 | 66 | unlink($inputFile); 67 | 68 | if ($code !== 0) 69 | { 70 | throw FilterException::fromProcess($process)->setInput($asset->getContent()); 71 | } 72 | 73 | $asset->setContent($process->getOutput()); 74 | } 75 | 76 | /** 77 | * Apply a filter on file dump. 78 | * 79 | * @param \Assetic\Asset\AssetInterface $asset 80 | * @return void 81 | */ 82 | public function filterDump(AssetInterface $asset){} 83 | 84 | } -------------------------------------------------------------------------------- /src/Basset/Filter/Filterable.php: -------------------------------------------------------------------------------- 1 | filters = new \Illuminate\Support\Collection; 22 | } 23 | 24 | /** 25 | * Syntatical sugar for chaining filters. 26 | * 27 | * @param string|array $filter 28 | * @param \Closure $callback 29 | * @return \Basset\Filter\Filter|\Basset\Filter\Filterable 30 | */ 31 | public function andApply($filter, Closure $callback = null) 32 | { 33 | return $this->apply($filter, $callback); 34 | } 35 | 36 | /** 37 | * Apply a filter. 38 | * 39 | * @param string|array $filter 40 | * @param \Closure $callback 41 | * @return \Basset\Filter\Filter|\Basset\Filter\Filterable 42 | */ 43 | public function apply($filter, Closure $callback = null) 44 | { 45 | // If the supplied filter is an array then we'll treat it as an array of filters that are 46 | // to be applied to the resource. 47 | if (is_array($filter)) 48 | { 49 | return $this->applyFromArray($filter); 50 | } 51 | 52 | $filter = $this->factory->get('filter')->make($filter)->setResource($this); 53 | 54 | is_callable($callback) and call_user_func($callback, $filter); 55 | 56 | return $this->filters[$filter->getFilter()] = $filter; 57 | } 58 | 59 | /** 60 | * Apply filter from an array of filters. 61 | * 62 | * @param array $filters 63 | * @return \Basset\Filter\Filterable 64 | */ 65 | public function applyFromArray($filters) 66 | { 67 | foreach ($filters as $key => $value) 68 | { 69 | $filter = $this->factory->get('filter')->make(is_callable($value) ? $key : $value)->setResource($this); 70 | 71 | is_callable($value) and call_user_func($value, $filter); 72 | 73 | $this->filters[$filter->getFilter()] = $filter; 74 | } 75 | 76 | return $this; 77 | } 78 | 79 | /** 80 | * Get the applied filters. 81 | * 82 | * @return \Illuminate\Support\Collection 83 | */ 84 | public function getFilters() 85 | { 86 | return $this->filters; 87 | } 88 | 89 | /** 90 | * Get the log writer instance. 91 | * 92 | * @return \Illumiante\Log\Writer 93 | */ 94 | public function getLogger() 95 | { 96 | return $this->factory->getLogger(); 97 | } 98 | 99 | } -------------------------------------------------------------------------------- /tests/Basset/Manifest/ManifestTest.php: -------------------------------------------------------------------------------- 1 | files = new Illuminate\Filesystem\Filesystem; 17 | $this->manifest = new Basset\Manifest\Manifest($this->files, __DIR__.'/fixtures'); 18 | } 19 | 20 | 21 | public function testManifestIsLoadedCorrectlyFromFilesystem() 22 | { 23 | $this->manifest->load(); 24 | 25 | $this->assertEquals(array( 26 | 'fingerprints' => array('stylesheets' => 'bar'), 27 | 'development' => array('stylesheets' => array('baz/qux.scss' => 'baz/qux.css')) 28 | ), $this->manifest->get('foo')->toArray()); 29 | } 30 | 31 | 32 | public function testGetInvalidManifestEntryReturnsNull() 33 | { 34 | $this->assertNull($this->manifest->get('foo')); 35 | } 36 | 37 | 38 | public function testMakeManifestEntryReturnsNewEntry() 39 | { 40 | $this->assertInstanceOf('Basset\Manifest\Entry', $this->manifest->make('foo')); 41 | } 42 | 43 | 44 | public function testMakeReturnsExistingManifestEntryIfEntryAlreadyExists() 45 | { 46 | $foo = $this->manifest->make('foo'); 47 | $this->assertEquals($foo, $this->manifest->make('foo')); 48 | } 49 | 50 | 51 | public function testManifestChecksForExistingEntry() 52 | { 53 | $this->assertFalse($this->manifest->has('foo')); 54 | $this->manifest->make('foo'); 55 | $this->assertTrue($this->manifest->has('foo')); 56 | } 57 | 58 | 59 | public function testManifestUsesCollectionInstanceToGetEntryName() 60 | { 61 | $collection = m::mock('Basset\Collection'); 62 | $collection->shouldReceive('getIdentifier')->once()->andReturn('foo'); 63 | $this->assertInstanceOf('Basset\Manifest\Entry', $this->manifest->make($collection)); 64 | } 65 | 66 | 67 | public function testWritesChangedEntriesToManifest() 68 | { 69 | $this->files = m::mock('Illuminate\Filesystem\Filesystem'); 70 | $this->manifest = new Basset\Manifest\Manifest($this->files, __DIR__.'/fixtures'); 71 | 72 | $entry = $this->manifest->make('foo'); 73 | $entry->setProductionFingerprint('stylesheets', 'foo-123.css'); 74 | 75 | $this->files->shouldReceive('put')->once()->with(__DIR__.'/fixtures/collections.json', json_encode(array('foo' => $entry->toArray())))->andReturn(true); 76 | 77 | $this->assertTrue($this->manifest->save()); 78 | } 79 | 80 | 81 | public function testNonDirtyManifestDoesNotSave() 82 | { 83 | $this->files = m::mock('Illuminate\Filesystem\Filesystem'); 84 | $this->manifest = new Basset\Manifest\Manifest($this->files, __DIR__.'/fixtures'); 85 | 86 | $this->assertFalse($this->manifest->save()); 87 | } 88 | 89 | 90 | } -------------------------------------------------------------------------------- /src/Basset/Factory/AssetFactory.php: -------------------------------------------------------------------------------- 1 | files = $files; 47 | $this->appEnvironment = $appEnvironment; 48 | $this->publicPath = $publicPath; 49 | } 50 | 51 | /** 52 | * Make a new asset instance. 53 | * 54 | * @param string $path 55 | * @return \Basset\Asset 56 | */ 57 | public function make($path) 58 | { 59 | $absolutePath = $this->buildAbsolutePath($path); 60 | 61 | $relativePath = $this->buildRelativePath($absolutePath); 62 | 63 | $asset = new Asset($this->files, $this->factory, $this->appEnvironment, $absolutePath, $relativePath); 64 | 65 | return $asset->setOrder($this->nextAssetOrder()); 66 | } 67 | 68 | /** 69 | * Build the absolute path to an asset. 70 | * 71 | * @param string $path 72 | * @return string 73 | */ 74 | public function buildAbsolutePath($path) 75 | { 76 | if (is_null($path)) return $path; 77 | 78 | return realpath($path) ?: $path; 79 | } 80 | 81 | /** 82 | * Build the relative path to an asset. 83 | * 84 | * @param string $path 85 | * @return string 86 | */ 87 | public function buildRelativePath($path) 88 | { 89 | if (is_null($path)) return $path; 90 | 91 | $relativePath = str_replace(array(realpath($this->publicPath), '\\'), array('', '/'), $path); 92 | 93 | // If the asset is not a remote asset then we'll trim the relative path even further to remove 94 | // any unnecessary leading or trailing slashes. This will leave us with a nice relative path. 95 | if ( ! starts_with($path, '//') and ! (bool) filter_var($path, FILTER_VALIDATE_URL)) 96 | { 97 | $relativePath = trim($relativePath, '/'); 98 | 99 | // If the given path is the same as the built relative path then the asset appears to be 100 | // outside of the public directory. If this is the case then we'll use an MD5 hash of 101 | // the assets path as the relative path to the asset. 102 | if (trim(str_replace('\\', '/', $path), '/') == trim($relativePath, '/')) 103 | { 104 | $path = pathinfo($path); 105 | 106 | $relativePath = md5($path['dirname']).'/'.$path['basename']; 107 | } 108 | } 109 | 110 | return $relativePath; 111 | } 112 | 113 | /** 114 | * Get the next asset order. 115 | * 116 | * @return int 117 | */ 118 | protected function nextAssetOrder() 119 | { 120 | return ++$this->assetsProduced; 121 | } 122 | 123 | } -------------------------------------------------------------------------------- /tests/Basset/Builder/FilesystemCleanerTest.php: -------------------------------------------------------------------------------- 1 | environment = m::mock('Basset\Environment'); 17 | $this->manifest = m::mock('Basset\Manifest\Manifest'); 18 | $this->files = m::mock('Illuminate\Filesystem\Filesystem'); 19 | 20 | $this->cleaner = new Basset\Builder\FilesystemCleaner($this->environment, $this->manifest, $this->files, 'path/to/builds'); 21 | 22 | $this->manifest->shouldReceive('save')->atLeast()->once(); 23 | } 24 | 25 | 26 | public function testForgettingCollectionFromManifestThatNoLongerExistsOnEnvironment() 27 | { 28 | $this->environment->shouldReceive('offsetExists')->once()->with('foo')->andReturn(false); 29 | $this->manifest->shouldReceive('get')->once()->with('foo')->andReturn($entry = m::mock('Basset\Manifest\Entry')); 30 | $this->manifest->shouldReceive('forget')->once()->with('foo'); 31 | 32 | $entry->shouldReceive('hasProductionFingerprints')->once()->andReturn(false); 33 | $this->files->shouldReceive('glob')->with('path/to/builds/foo-*.*')->andReturn(array('path/to/builds/foo-123.css')); 34 | $this->files->shouldReceive('delete')->with('path/to/builds/foo-123.css'); 35 | $entry->shouldReceive('resetProductionFingerprints')->once(); 36 | 37 | $entry->shouldReceive('hasDevelopmentAssets')->once()->andReturn(false); 38 | $this->files->shouldReceive('deleteDirectory')->once()->with('path/to/builds/foo'); 39 | $entry->shouldReceive('resetDevelopmentAssets')->once(); 40 | 41 | $this->cleaner->clean('foo'); 42 | } 43 | 44 | 45 | public function testCleaningOfManifestFilesOnFilesystem() 46 | { 47 | $this->manifest->shouldReceive('get')->once()->with('foo')->andReturn($entry = m::mock('Basset\Manifest\Entry')); 48 | $this->environment->shouldReceive('offsetExists')->times(3)->with('foo')->andReturn(true); 49 | $this->environment->shouldReceive('offsetGet')->once()->with('foo')->andReturn($collection = m::mock('Basset\Collection')); 50 | 51 | $collection->shouldReceive('getIdentifier')->twice()->andReturn('foo'); 52 | 53 | $entry->shouldReceive('getProductionFingerprints')->once()->andReturn(array( 54 | 'foo-37b51d194a7513e45b56f6524f2d51f2.css', 55 | 'bar-acbd18db4cc2f85cedef654fccc4a4d8.js' 56 | )); 57 | 58 | $this->files->shouldReceive('glob')->once()->with('path/to/builds/foo-*.css')->andReturn(array( 59 | 'path/to/builds/foo-37b51d194a7513e45b56f6524f2d51f2.css', 60 | 'path/to/builds/foo-asfjkb8912h498hacn8casc8h8942102.css' 61 | )); 62 | $this->files->shouldReceive('delete')->once()->with('path/to/builds/foo-asfjkb8912h498hacn8casc8h8942102.css')->andReturn(true); 63 | $this->files->shouldReceive('glob')->once()->with('path/to/builds/bar-*.js')->andReturn(array()); 64 | 65 | $entry->shouldReceive('getDevelopmentAssets')->once()->andReturn(array( 66 | 'stylesheets' => array('bar/baz-37b51d194a7513e45b56f6524f2d51f2.css', 'bar/qux-acbd18db4cc2f85cedef654fccc4a4d8.css'), 67 | 'javascripts' => array() 68 | )); 69 | 70 | $this->files->shouldReceive('glob')->once()->with('path/to/builds/foo/bar/baz-*.css')->andReturn(array('path/to/builds/foo/bar/baz-37b51d194a7513e45b56f6524f2d51f2.css')); 71 | $this->files->shouldReceive('glob')->once()->with('path/to/builds/foo/bar/qux-*.css')->andReturn(array()); 72 | 73 | $entry->shouldReceive('hasProductionFingerprints')->once()->andReturn(true); 74 | $entry->shouldReceive('hasDevelopmentAssets')->once()->andReturn(true); 75 | 76 | $this->cleaner->clean('foo'); 77 | } 78 | 79 | 80 | } -------------------------------------------------------------------------------- /tests/Basset/CollectionTest.php: -------------------------------------------------------------------------------- 1 | collection = new Collection($this->directory = m::mock('Basset\Directory'), 'foo'); 20 | } 21 | 22 | 23 | public function testGetIdentifierOfCollection() 24 | { 25 | $this->assertEquals('foo', $this->collection->getIdentifier()); 26 | } 27 | 28 | 29 | public function testGetDefaultDirectory() 30 | { 31 | $this->assertEquals($this->directory, $this->collection->getDefaultDirectory()); 32 | } 33 | 34 | 35 | public function testGetExtensionFromGroup() 36 | { 37 | $this->assertEquals('css', $this->collection->getExtension('stylesheets')); 38 | $this->assertEquals('js', $this->collection->getExtension('javascripts')); 39 | } 40 | 41 | 42 | public function testGettingCollectionAssetsWithDefaultOrdering() 43 | { 44 | $this->directory->shouldReceive('getAssets')->andReturn($expected = new IlluminateCollection(array( 45 | $this->getAssetInstance('bar.css', 'path/to/bar.css', 'stylesheets', 1), 46 | $this->getAssetInstance('baz.css', 'path/to/baz.css', 'stylesheets', 2) 47 | ))); 48 | 49 | $this->assertEquals($expected->all(), $this->collection->getAssets('stylesheets')->all()); 50 | } 51 | 52 | 53 | public function testGettingCollectionWithMultipleAssetGroupsReturnsOnlyRequestedGroup() 54 | { 55 | $this->directory->shouldReceive('getAssets')->andReturn(new IlluminateCollection(array( 56 | $assets[] = $this->getAssetInstance('foo.css', 'path/to/foo.css', 'stylesheets', 1), 57 | $assets[] = $this->getAssetInstance('bar.js', 'path/to/bar.js', 'javascripts', 2), 58 | $assets[] = $this->getAssetInstance('baz.js', 'path/to/baz.js', 'javascripts', 3), 59 | $assets[] = $this->getAssetInstance('qux.css', 'path/to/qux.css', 'stylesheets', 4) 60 | ))); 61 | 62 | $expected = array(0 => $assets[0], 3 => $assets[3]); 63 | $this->assertEquals($expected, $this->collection->getAssets('stylesheets')->all()); 64 | } 65 | 66 | 67 | public function testGettingCollectionAssetsWithCustomOrdering() 68 | { 69 | $this->directory->shouldReceive('getAssets')->andReturn(new IlluminateCollection(array( 70 | $assets[] = $this->getAssetInstance('foo.css', 'path/to/foo.css', 'stylesheets', 1), // Becomes 2nd 71 | $assets[] = $this->getAssetInstance('bar.css', 'path/to/bar.css', 'stylesheets', 2), // Becomes 4th 72 | $assets[] = $this->getAssetInstance('baz.css', 'path/to/baz.css', 'stylesheets', 1), // Becomes 1st 73 | $assets[] = $this->getAssetInstance('qux.css', 'path/to/qux.css', 'stylesheets', 4), // Becomes 5th 74 | $assets[] = $this->getAssetInstance('zin.css', 'path/to/zin.css', 'stylesheets', 3) // Becomes 3rd 75 | ))); 76 | 77 | $expected = array($assets[2], $assets[0], $assets[4], $assets[1], $assets[3]); 78 | $this->assertEquals($expected, $this->collection->getAssets('stylesheets')->all()); 79 | } 80 | 81 | 82 | public function testGettingCollectionRawAssets() 83 | { 84 | $this->directory->shouldReceive('getAssets')->andReturn(new IlluminateCollection(array( 85 | $assets[] = $this->getAssetInstance('foo.css', 'path/to/foo.css', 'stylesheets', 1), 86 | $assets[] = $this->getAssetInstance('bar.css', 'path/to/bar.css', 'stylesheets', 2) 87 | ))); 88 | 89 | $assets[1]->raw(); 90 | 91 | $this->assertEquals(array(1 => $assets[1]), $this->collection->getAssetsOnlyRaw('stylesheets')->all()); 92 | } 93 | 94 | 95 | public function getAssetInstance($relative, $absolute, $group, $order) 96 | { 97 | $asset = new Asset(m::mock('Illuminate\Filesystem\Filesystem'), m::mock('Basset\Factory\FactoryManager'), 'testing', $absolute, $relative); 98 | 99 | return $asset->setOrder($order)->setGroup($group); 100 | } 101 | 102 | 103 | } -------------------------------------------------------------------------------- /src/Basset/Console/BassetCommand.php: -------------------------------------------------------------------------------- 1 | manifest = $manifest; 67 | $this->environment = $environment; 68 | $this->cleaner = $cleaner; 69 | } 70 | 71 | /** 72 | * Execute the console command. 73 | * 74 | * @return void 75 | */ 76 | public function fire() 77 | { 78 | if ( ! $this->input->getOption('delete-manifest') and ! $this->input->getOption('tidy-up')) 79 | { 80 | $this->line('Basset version '.Basset::VERSION.''); 81 | } 82 | else 83 | { 84 | if ($this->input->getOption('delete-manifest')) 85 | { 86 | $this->deleteCollectionManifest(); 87 | } 88 | 89 | if ($this->input->getOption('tidy-up')) 90 | { 91 | $this->tidyUpFilesystem(); 92 | } 93 | } 94 | } 95 | 96 | /** 97 | * Delete the collection manifest. 98 | * 99 | * @return void 100 | */ 101 | protected function deleteCollectionManifest() 102 | { 103 | if ($this->manifest->delete()) 104 | { 105 | $this->info('Manifest has been deleted. All collections will are now required to be rebuilt.'); 106 | } 107 | else 108 | { 109 | $this->comment('Manifest does not exist or could not be deleted.'); 110 | } 111 | } 112 | 113 | /** 114 | * Tidy up the filesystem with the build cleaner. 115 | * 116 | * @return void 117 | */ 118 | protected function tidyUpFilesystem() 119 | { 120 | $collections = array_keys($this->environment->all()) + array_keys($this->manifest->all()); 121 | 122 | foreach ($collections as $collection) 123 | { 124 | if ($this->input->getOption('verbose')) 125 | { 126 | $this->line('['.$collection.'] Cleaning up files and manifest entries.'); 127 | } 128 | 129 | $this->cleaner->clean($collection); 130 | } 131 | 132 | $this->input->getOption('verbose') and $this->line(''); 133 | 134 | $this->info('The filesystem and manifest have been tidied up.'); 135 | } 136 | 137 | /** 138 | * Get the console command options. 139 | * 140 | * @return array 141 | */ 142 | protected function getOptions() 143 | { 144 | return array( 145 | array('delete-manifest', null, InputOption::VALUE_NONE, 'Delete the collection manifest'), 146 | array('tidy-up', null, InputOption::VALUE_NONE, 'Tidy up the outdated collections and manifest entries') 147 | ); 148 | } 149 | 150 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## No Longer Maintained 2 | 3 | Basset is no longer being maintained by me (Jason Lewis). Appologies to those of you that have invested time into this package. Feel free to fork it if 4 | you feel the need but I strongly urged you to shift over to using [Grunt](http://gruntjs.com/) to manage the compiling and concatenation of your assets. 5 | 6 | Once again, I'm sorry, I just don't have the time and Grunt does an amazing job. 7 | 8 | ## Basset for Laravel 4 9 | 10 | [![Build Status](https://secure.travis-ci.org/jasonlewis/basset.png)](http://travis-ci.org/jasonlewis/basset) 11 | 12 | Basset is a better asset management package for the Laravel framework. Basset shares the same philosophy as Laravel. Development should be an enjoyable and fulfilling experience. When it comes to managing your assets it can become quite complex and a pain in the backside. These days developers are able to use a range of pre-processors such as Sass, Less, and CoffeeScript. Basset is able to handle the processing of these assets instead of relying on a number of individual tools. 13 | 14 | ### Installation 15 | 16 | - [Basset on Packagist](https://packagist.org/packages/jasonlewis/basset) 17 | - [Basset on GitHub](https://github.com/jasonlewis/basset) 18 | 19 | To get the latest version of Basset simply require it in your `composer.json` file. 20 | 21 | ~~~ 22 | "jasonlewis/basset": "dev-master" 23 | ~~~ 24 | 25 | You'll then need to run `composer install` to download it and have the autoloader updated. 26 | 27 | > Note that once Basset has a stable version tagged you should use a tagged release instead of the master branch. 28 | 29 | Once Basset is installed you need to register the service provider with the application. Open up `app/config/app.php` and find the `providers` key. 30 | 31 | ~~~ 32 | 'providers' => array( 33 | 34 | 'Basset\BassetServiceProvider' 35 | 36 | ) 37 | ~~~ 38 | 39 | Basset also ships with a facade which provides the static syntax for creating collections. You can register the facade in the `aliases` key of your `app/config/app.php` file. 40 | 41 | ~~~ 42 | 'aliases' => array( 43 | 44 | 'Basset' => 'Basset\Facade' 45 | 46 | ) 47 | ~~~ 48 | 49 | ### Documentation 50 | 51 | [View the official documentation](http://jasonlewis.me/code/basset/4.0). 52 | 53 | ### Changes 54 | 55 | #### v4.0.0 Beta 3 56 | 57 | - Split the collections and aliases into their own configuration files. 58 | - Filter method chaining with syntactical sugar by prefixing with `and`, e.g., `andWhenProductionBuild()`. 59 | 60 | #### v4.0.0 Beta 2 61 | 62 | - Added logging when assets, directories, and filters are not found or fail to load. 63 | - Allow logging to be enabled or disabled via configuration. 64 | - Warn users when cURL is being used to detect an assets group. 65 | - Allow an array of filters to be applied to an asset. 66 | - Added `whenProductionBuild` and `whenDevelopmentBuild` as filter requirements. 67 | - `CssMin` and `JsMin` are only applied on a production build and not on the production environment. 68 | - Added `raw` method as an alias to `exclude`. 69 | - Entire directory or collection can be set as raw so original path is used instead of assets being built. 70 | - Development builds only happen for a collection that is used on the loaded request. 71 | - Added `rawOnEnvironment` to serve the asset raw on a given environment or environments. 72 | 73 | 74 | #### v4.0.0 Beta 1 75 | 76 | - Collections are displayed with `basset_javascripts()` and `basset_stylesheets()`. 77 | - Simplified the asset finding process. 78 | - Can no longer prefix paths with `path:` for an absolute path, use a relative path from public directory instead. 79 | - Requirements can be applied to filters to prevent application if certain conditions are not met. 80 | - Filters can find any missing constructor arguments such as the path to Node, Ruby, etc. 81 | - Default `application` collection is bundled. 82 | - `basset:compile` command is now `basset:build`. 83 | - Old collection builds are cleaned automatically but can be cleaned manually with `basset --tidy-up`. 84 | - Packages can be registered with `Basset::package()` and assets can be added using the familiar namespace syntax found throughout Laravel. 85 | - `Csso` support with `CssoFilter`. 86 | - Fixed issues with `UriRewriteFilter`. 87 | - Development collections are pre-built before every page load. 88 | - Build and serve pre-compressed collections. 89 | - Use custom format when displaying collections. 90 | - Added in Blade view helpers: `@javascripts`, `@stylesheets`, and `@assets`. 91 | - Assets maintain the order that they were added. 92 | -------------------------------------------------------------------------------- /src/Basset/Manifest/Manifest.php: -------------------------------------------------------------------------------- 1 | files = $files; 46 | $this->manifestPath = $manifestPath; 47 | $this->entries = new \Illuminate\Support\Collection; 48 | } 49 | 50 | /** 51 | * Determine if the manifest has a given collection entry. 52 | * 53 | * @param string $collection 54 | * @return bool 55 | */ 56 | public function has($collection) 57 | { 58 | return ! is_null($this->get($collection)); 59 | } 60 | 61 | /** 62 | * Get a collection entry from the manifest or create a new entry. 63 | * 64 | * @param string|\Basset\Collection $collection 65 | * @return null|\Basset\Manifest\Entry 66 | */ 67 | public function get($collection) 68 | { 69 | $collection = $this->getCollectionNameFromInstance($collection); 70 | 71 | return isset($this->entries[$collection]) ? $this->entries[$collection] : null; 72 | } 73 | 74 | /** 75 | * Make a collection entry if it does not already exist on the manifest. 76 | * 77 | * @param string|\Basset\Collection $collection 78 | * @return \Basset\Manifest\Entry 79 | */ 80 | public function make($collection) 81 | { 82 | $collection = $this->getCollectionNameFromInstance($collection); 83 | 84 | $this->dirty = true; 85 | 86 | return $this->get($collection) ?: $this->entries[$collection] = new Entry; 87 | } 88 | 89 | /** 90 | * Forget a collection from the repository. 91 | * 92 | * @param string|\Basset\Collection $collection 93 | * @return void 94 | */ 95 | public function forget($collection) 96 | { 97 | $collection = $this->getCollectionNameFromInstance($collection); 98 | 99 | if ($this->has($collection)) 100 | { 101 | $this->dirty = true; 102 | 103 | unset($this->entries[$collection]); 104 | } 105 | } 106 | 107 | /** 108 | * Get all the entries. 109 | * 110 | * @return array 111 | */ 112 | public function all() 113 | { 114 | return $this->entries; 115 | } 116 | 117 | /** 118 | * Get the collections identifier from a collection instance. 119 | * 120 | * @param string|\Basset\Collection $collection 121 | * @return string 122 | */ 123 | protected function getCollectionNameFromInstance($collection) 124 | { 125 | return $collection instanceof Collection ? $collection->getIdentifier() : $collection; 126 | } 127 | 128 | /** 129 | * Loads and registers the manifest entries. 130 | * 131 | * @return void 132 | */ 133 | public function load() 134 | { 135 | $path = $this->manifestPath.'/collections.json'; 136 | 137 | if ($this->files->exists($path) and is_array($manifest = json_decode($this->files->get($path), true))) 138 | { 139 | foreach ($manifest as $key => $entry) 140 | { 141 | $entry = new Entry($entry['fingerprints'], $entry['development']); 142 | 143 | $this->entries->put($key, $entry); 144 | } 145 | } 146 | } 147 | 148 | /** 149 | * Save the manifest. 150 | * 151 | * @return bool 152 | */ 153 | public function save() 154 | { 155 | if ($this->dirty) 156 | { 157 | $path = $this->manifestPath.'/collections.json'; 158 | 159 | $this->dirty = false; 160 | 161 | return (bool) $this->files->put($path, $this->entries->toJson()); 162 | } 163 | 164 | return false; 165 | } 166 | 167 | /** 168 | * Delete the manifest. 169 | * 170 | * @return bool 171 | */ 172 | public function delete() 173 | { 174 | if ($this->files->exists($path = $this->manifestPath.'/collections.json')) 175 | { 176 | return $this->files->delete($path); 177 | } 178 | else 179 | { 180 | return false; 181 | } 182 | } 183 | 184 | } -------------------------------------------------------------------------------- /src/Basset/Collection.php: -------------------------------------------------------------------------------- 1 | directory = $directory; 29 | $this->identifier = $identifier; 30 | } 31 | 32 | /** 33 | * Get all the assets filtered by a group and without the raw assets. 34 | * 35 | * @param string $group 36 | * @return \Illuminate\Support\Collection 37 | */ 38 | public function getAssetsWithoutRaw($group = null) 39 | { 40 | return $this->getAssets($group, false); 41 | } 42 | 43 | /** 44 | * Get all the assets filtered by a group and with the raw assets. 45 | * 46 | * @param string $group 47 | * @return \Illuminate\Support\Collection 48 | */ 49 | public function getAssetsWithRaw($group = null) 50 | { 51 | return $this->getAssets($group, true); 52 | } 53 | 54 | /** 55 | * Get all the assets filtered by a group but only if the assets are raw. 56 | * 57 | * @param string $group 58 | * @return \Illuminate\Support\Collection 59 | */ 60 | public function getAssetsOnlyRaw($group = null) 61 | { 62 | // Get all the assets for the given group and filter out assets that aren't listed 63 | // as being raw. 64 | $assets = $this->getAssets($group, true)->filter(function($asset) 65 | { 66 | return $asset->isRaw(); 67 | }); 68 | 69 | return $assets; 70 | } 71 | 72 | /** 73 | * Get all the assets filtered by a group and if to include the raw assets. 74 | * 75 | * @param string $group 76 | * @param bool $raw 77 | * @return \Illuminate\Support\Collection 78 | */ 79 | public function getAssets($group = null, $raw = true) 80 | { 81 | // Spin through all of the assets that belong to the given group and push them on 82 | // to the end of the array. 83 | $assets = clone $this->directory->getAssets(); 84 | 85 | foreach ($assets as $key => $asset) 86 | { 87 | if ( ! $raw and $asset->isRaw() or ! is_null($group) and ! $asset->{'is'.ucfirst(str_singular($group))}()) 88 | { 89 | $assets->forget($key); 90 | } 91 | } 92 | 93 | // Spin through each of the assets and build an ordered array of assets. Once 94 | // we have the ordered array we'll transform it into a collection and apply 95 | // the collection wide filters to each asset. 96 | $ordered = array(); 97 | 98 | foreach ($assets as $asset) 99 | { 100 | $this->orderAsset($asset, $ordered); 101 | } 102 | 103 | return new \Illuminate\Support\Collection($ordered); 104 | } 105 | 106 | /** 107 | * Orders the array of assets as they were defined or on a user ordered basis. 108 | * 109 | * @param \Basset\Asset $asset 110 | * @param array $assets 111 | * @return void 112 | */ 113 | protected function orderAsset(Asset $asset, array &$assets) 114 | { 115 | $order = $asset->getOrder() and $order--; 116 | 117 | // If an asset already exists at the given order key then we'll add one to the order 118 | // so the asset essentially appears after the existing asset. This makes sense since 119 | // the array of assets has been reversed, so if the last asset was told to be first 120 | // then when we finally get to the first added asset it's added second. 121 | if (array_key_exists($order, $assets)) 122 | { 123 | array_splice($assets, $order, 0, array(null)); 124 | } 125 | 126 | $assets[$order] = $asset; 127 | 128 | ksort($assets); 129 | } 130 | 131 | /** 132 | * Get the identifier of the collection. 133 | * 134 | * @return string 135 | */ 136 | public function getIdentifier() 137 | { 138 | return $this->identifier; 139 | } 140 | 141 | /** 142 | * Get the default directory. 143 | * 144 | * @return \Basset\Directory 145 | */ 146 | public function getDefaultDirectory() 147 | { 148 | return $this->directory; 149 | } 150 | 151 | /** 152 | * Determine an extension based on the group. 153 | * 154 | * @param string $group 155 | * @return string 156 | */ 157 | public function getExtension($group) 158 | { 159 | return str_plural($group) == 'stylesheets' ? 'css' : 'js'; 160 | } 161 | 162 | /** 163 | * Dynamically call methods on the default directory. 164 | * 165 | * @param string $method 166 | * @param array $parameters 167 | * @return mixed 168 | */ 169 | public function __call($method, $parameters) 170 | { 171 | return call_user_func_array(array($this->directory, $method), $parameters); 172 | } 173 | 174 | } -------------------------------------------------------------------------------- /src/Basset/Environment.php: -------------------------------------------------------------------------------- 1 | factory = $factory; 41 | $this->finder = $finder; 42 | } 43 | 44 | /** 45 | * Alias of \Basset\Environment::collection() 46 | * 47 | * @param string $name 48 | * @param \Closure $callback 49 | * @return \Basset\Collection 50 | */ 51 | public function make($name, Closure $callback = null) 52 | { 53 | return $this->collection($name, $callback); 54 | } 55 | 56 | /** 57 | * Create or return an existing collection. 58 | * 59 | * @param string $identifier 60 | * @param \Closure $callback 61 | * @return \Basset\Collection 62 | */ 63 | public function collection($identifier, Closure $callback = null) 64 | { 65 | if ( ! isset($this->collections[$identifier])) 66 | { 67 | $directory = $this->prepareDefaultDirectory(); 68 | 69 | $this->collections[$identifier] = new Collection($directory, $identifier); 70 | } 71 | 72 | // If the collection has been given a callable closure then we'll execute the closure with 73 | // the collection instance being the only parameter given. This allows users to begin 74 | // using the collection instance to add assets. 75 | if (is_callable($callback)) 76 | { 77 | call_user_func($callback, $this->collections[$identifier]); 78 | } 79 | 80 | return $this->collections[$identifier]; 81 | } 82 | 83 | /** 84 | * Prepare the default directory for a new collection. 85 | * 86 | * @return \Basset\Directory 87 | */ 88 | protected function prepareDefaultDirectory() 89 | { 90 | $path = $this->finder->setWorkingDirectory('/'); 91 | 92 | return new Directory($this->factory, $this->finder, $path); 93 | } 94 | 95 | /** 96 | * Get all collections. 97 | * 98 | * @return array 99 | */ 100 | public function all() 101 | { 102 | return $this->collections; 103 | } 104 | 105 | /** 106 | * Determine if a collection exists. 107 | * 108 | * @param string $name 109 | * @return bool 110 | */ 111 | public function has($name) 112 | { 113 | return isset($this->collections[$name]); 114 | } 115 | 116 | /** 117 | * Register a package with the environment. 118 | * 119 | * @param string $package 120 | * @param string $namespace 121 | * @return void 122 | */ 123 | public function package($package, $namespace = null) 124 | { 125 | if (is_null($namespace)) 126 | { 127 | list($vendor, $namespace) = explode('/', $package); 128 | } 129 | 130 | $this->finder->addNamespace($namespace, $package); 131 | } 132 | 133 | /** 134 | * Register an array of collections. 135 | * 136 | * @param array $collections 137 | * @return void 138 | */ 139 | public function collections(array $collections) 140 | { 141 | foreach ($collections as $name => $callback) 142 | { 143 | $this->make($name, $callback); 144 | } 145 | } 146 | 147 | /** 148 | * Set a collection offset. 149 | * 150 | * @param string $offset 151 | * @param mixed $value 152 | * @return void 153 | */ 154 | public function offsetSet($offset, $value) 155 | { 156 | if (is_null($offset)) 157 | { 158 | throw new InvalidArgumentException('Collection identifier not given.'); 159 | } 160 | 161 | $this->collection($offset, $value); 162 | } 163 | 164 | /** 165 | * Get a collection offset. 166 | * 167 | * @param string $offset 168 | * @return null|\Basset\Collection 169 | */ 170 | public function offsetGet($offset) 171 | { 172 | return $this->has($offset) ? $this->collection($offset) : null; 173 | } 174 | 175 | /** 176 | * Unset a collection offset. 177 | * 178 | * @param string $offset 179 | * @return void 180 | */ 181 | public function offsetUnset($offset) 182 | { 183 | unset($this->collections[$offset]); 184 | } 185 | 186 | /** 187 | * Determine if a collection offset exists. 188 | * 189 | * @param string $offset 190 | * @return bool 191 | */ 192 | public function offsetExists($offset) 193 | { 194 | return $this->has($offset); 195 | } 196 | 197 | } -------------------------------------------------------------------------------- /tests/Basset/Manifest/EntryTest.php: -------------------------------------------------------------------------------- 1 | data = array( 19 | 'fingerprints' => array( 20 | 'stylesheets' => 'foo-123.css' 21 | ), 22 | 'development' => array( 23 | 'stylesheets' => array( 24 | 'bar/baz.sass' => 'bar/baz.css' 25 | ), 26 | 'javascripts' => array( 27 | 'baz/qux.coffee' => 'baz/qux.js' 28 | ) 29 | ) 30 | ); 31 | 32 | $this->entry = new Entry($this->data['fingerprints'], $this->data['development']); 33 | } 34 | 35 | 36 | public function testDefaultArrayIsParsedCorrectly() 37 | { 38 | $this->assertEquals($this->data, $this->entry->toArray()); 39 | } 40 | 41 | 42 | public function testAddingDevelopmentAssetToEntry() 43 | { 44 | $this->entry->addDevelopmentAsset('foo/bar.sass', 'foo/bar.css', 'stylesheets'); 45 | $this->data['development']['stylesheets']['foo/bar.sass'] = 'foo/bar.css'; 46 | $this->assertEquals($this->data, $this->entry->toArray()); 47 | } 48 | 49 | 50 | public function testAddingDevelopmentAssetToEntryFromAssetInstance() 51 | { 52 | $asset = new Asset($files = m::mock('Illuminate\Filesystem\Filesystem'), m::mock('Basset\Factory\FactoryManager'), 'testing', 'foo/bar.sass', 'foo/bar.sass'); 53 | $files->shouldReceive('lastModified')->once()->with('foo/bar.sass')->andReturn(time()); 54 | $this->entry->addDevelopmentAsset($asset); 55 | $this->data['development']['stylesheets']['foo/bar.sass'] = 'foo/bar-'.md5('[]'.time()).'.css'; 56 | $this->assertEquals($this->data, $this->entry->toArray()); 57 | } 58 | 59 | 60 | public function testGettingDevelopmentAsset() 61 | { 62 | $this->assertEquals('bar/baz.css', $this->entry->getDevelopmentAsset('bar/baz.sass', 'stylesheets')); 63 | } 64 | 65 | 66 | public function testGettingInvalidDevelopmentAssetReturnsNull() 67 | { 68 | $this->assertNull($this->entry->getDevelopmentAsset('foo/bar.sass', 'stylesheets')); 69 | } 70 | 71 | 72 | public function testGettingDevelopmentAssetFromAssetInstance() 73 | { 74 | $asset = m::mock('Basset\Asset'); 75 | $asset->shouldReceive('getGroup')->once()->andReturn('stylesheets'); 76 | $asset->shouldReceive('getRelativePath')->once()->andReturn('bar/baz.sass'); 77 | $this->assertEquals('bar/baz.css', $this->entry->getDevelopmentAsset($asset)); 78 | } 79 | 80 | 81 | public function testCheckingForDevelopmentAssetExistence() 82 | { 83 | $this->assertFalse($this->entry->hasDevelopmentAsset('foo/bar.css', 'stylesheets')); 84 | $this->assertTrue($this->entry->hasDevelopmentAsset('bar/baz.sass', 'stylesheets')); 85 | } 86 | 87 | 88 | public function testGetAllDevelopmentAssets() 89 | { 90 | $this->assertEquals($this->data['development'], $this->entry->getDevelopmentAssets()); 91 | } 92 | 93 | 94 | public function testGetAllDevelopmentAssetsForGivenGroup() 95 | { 96 | $this->assertEquals($this->data['development']['javascripts'], $this->entry->getDevelopmentAssets('javascripts')); 97 | } 98 | 99 | 100 | public function testCheckingForExistenceOfAnyDevelopmentAssets() 101 | { 102 | $this->assertTrue($this->entry->hasDevelopmentAssets()); 103 | } 104 | 105 | 106 | public function testCheckingForExistenceOfSpecificDevelopmentAssetsGroup() 107 | { 108 | $this->entry->resetDevelopmentAssets('javascripts'); 109 | $this->assertFalse($this->entry->hasDevelopmentAssets('javascripts')); 110 | } 111 | 112 | 113 | public function testResettingAllDevelopmentAssets() 114 | { 115 | $this->entry->resetDevelopmentAssets(); 116 | $this->assertEmpty($this->entry->getDevelopmentAssets()); 117 | } 118 | 119 | 120 | public function testResettingSpecificDevelopmentAssetsGroup() 121 | { 122 | $this->entry->resetDevelopmentAssets('javascripts'); 123 | $this->assertEmpty($this->entry->getDevelopmentAssets('javascripts')); 124 | } 125 | 126 | 127 | public function testSettingProductionFingerprintOnGroup() 128 | { 129 | $this->entry->setProductionFingerprint('javascripts', 'foo-321.js'); 130 | $this->assertEquals('foo-321.js', $this->entry->getProductionFingerprint('javascripts')); 131 | } 132 | 133 | 134 | public function testCheckingForExistenceOfFingerprint() 135 | { 136 | $this->assertTrue($this->entry->hasProductionFingerprint('stylesheets')); 137 | $this->assertFalse($this->entry->hasProductionFingerprint('javascripts')); 138 | } 139 | 140 | 141 | public function testGettingAllProductionFingerprints() 142 | { 143 | $this->assertEquals($this->data['fingerprints'], $this->entry->getProductionFingerprints()); 144 | } 145 | 146 | 147 | public function testGettingEntryAsJson() 148 | { 149 | $this->assertEquals(json_encode($this->data), $this->entry->toJson()); 150 | } 151 | 152 | 153 | public function testGettingEntryAsArray() 154 | { 155 | $this->assertEquals($this->data, $this->entry->toArray()); 156 | } 157 | 158 | 159 | } -------------------------------------------------------------------------------- /src/Basset/Manifest/Entry.php: -------------------------------------------------------------------------------- 1 | fingerprints = $fingerprints; 33 | $this->development = $development; 34 | } 35 | 36 | /** 37 | * Add a development asset. 38 | * 39 | * @param string|\Basset\Asset $value 40 | * @param string $fingerprint 41 | * @param string $group 42 | * @return void 43 | */ 44 | public function addDevelopmentAsset($value, $fingerprint = null, $group = null) 45 | { 46 | if ($value instanceof Asset) 47 | { 48 | $group = $value->getGroup(); 49 | 50 | $fingerprint = $value->getBuildPath(); 51 | 52 | $value = $value->getRelativePath(); 53 | } 54 | 55 | $this->development[$group][$value] = $fingerprint; 56 | } 57 | 58 | /** 59 | * Get a development assets build path. 60 | * 61 | * @param string|\Basset\Asset $value 62 | * @param string $group 63 | * @return null|string 64 | */ 65 | public function getDevelopmentAsset($value, $group = null) 66 | { 67 | if ($value instanceof Asset) 68 | { 69 | $group = $value->getGroup(); 70 | 71 | $value = $value->getRelativePath(); 72 | } 73 | 74 | return isset($this->development[$group][$value]) ? $this->development[$group][$value] : null; 75 | } 76 | 77 | /** 78 | * Determine if the entry has a development asset. 79 | * 80 | * @param string|\Basset\Asset $value 81 | * @param string $group 82 | * @return bool 83 | */ 84 | public function hasDevelopmentAsset($value, $group = null) 85 | { 86 | return ! is_null($this->getDevelopmentAsset($value, $group)); 87 | } 88 | 89 | /** 90 | * Get all or a subset of development assets. 91 | * 92 | * @param string $group 93 | * @return array 94 | */ 95 | public function getDevelopmentAssets($group = null) 96 | { 97 | return is_null($group) ? $this->development : $this->development[$group]; 98 | } 99 | 100 | /** 101 | * Determine if the entry has any development assets. 102 | * 103 | * @param string $group 104 | * @return bool 105 | */ 106 | public function hasDevelopmentAssets($group = null) 107 | { 108 | return is_null($group) ? ! empty($this->development) : ! empty($this->development[$group]); 109 | } 110 | 111 | /** 112 | * Reset the development assets. 113 | * 114 | * @param string $group 115 | * @return void 116 | */ 117 | public function resetDevelopmentAssets($group = null) 118 | { 119 | if (is_null($group)) 120 | { 121 | $this->development = array(); 122 | } 123 | else 124 | { 125 | $this->development[$group] = array(); 126 | } 127 | } 128 | 129 | /** 130 | * Set the entry fingerprint. 131 | * 132 | * @param string $group 133 | * @param string $fingerprint 134 | * @return void 135 | */ 136 | public function setProductionFingerprint($group, $fingerprint) 137 | { 138 | $this->fingerprints[$group] = $fingerprint; 139 | } 140 | 141 | /** 142 | * Determine if entry has a fingerprint. 143 | * 144 | * @param string $group 145 | * @return bool 146 | */ 147 | public function hasProductionFingerprint($group) 148 | { 149 | return ! is_null($this->getProductionFingerprint($group)); 150 | } 151 | 152 | /** 153 | * Determine if entry has any fingerprints. 154 | * 155 | * @return bool 156 | */ 157 | public function hasProductionFingerprints() 158 | { 159 | return $this->hasProductionFingerprint('stylesheets') or $this->hasProductionFingerprint('javascripts'); 160 | } 161 | 162 | /** 163 | * Get the entry fingerprint. 164 | * 165 | * @param string $group 166 | * @return string|null 167 | */ 168 | public function getProductionFingerprint($group) 169 | { 170 | return isset($this->fingerprints[$group]) ? $this->fingerprints[$group] : null; 171 | } 172 | 173 | /** 174 | * Get all entry fingerprints. 175 | * 176 | * @return array 177 | */ 178 | public function getProductionFingerprints() 179 | { 180 | return $this->fingerprints; 181 | } 182 | 183 | /** 184 | * Reset a production fingerprint. 185 | * 186 | * @param string $group 187 | * @return void 188 | */ 189 | public function resetProductionFingerprint($group) 190 | { 191 | $this->fingerprints[$group] = null; 192 | } 193 | 194 | /** 195 | * Reset all production fingerprints. 196 | * 197 | * @return void 198 | */ 199 | public function resetProductionFingerprints() 200 | { 201 | $this->fingerprints = array(); 202 | } 203 | 204 | /** 205 | * Convert the entry to its JSON representation. 206 | * 207 | * @param int $options 208 | * @return string 209 | */ 210 | public function toJson($options = 0) 211 | { 212 | return json_encode($this->toArray(), $options); 213 | } 214 | 215 | /** 216 | * Convert the entry to its array representation. 217 | * 218 | * @return array 219 | */ 220 | public function toArray() 221 | { 222 | return get_object_vars($this); 223 | } 224 | 225 | } -------------------------------------------------------------------------------- /src/Basset/Console/BuildCommand.php: -------------------------------------------------------------------------------- 1 | environment = $environment; 63 | $this->builder = $builder; 64 | $this->cleaner = $cleaner; 65 | } 66 | 67 | /** 68 | * Execute the console command. 69 | * 70 | * @return void 71 | */ 72 | public function fire() 73 | { 74 | $this->input->getOption('force') and $this->builder->setForce(true); 75 | 76 | $this->input->getOption('gzip') and $this->builder->setGzip(true); 77 | 78 | if ($production = $this->input->getOption('production')) 79 | { 80 | $this->comment('Starting production build...'); 81 | } 82 | else 83 | { 84 | $this->comment('Starting development build...'); 85 | } 86 | 87 | $collections = $this->gatherCollections(); 88 | 89 | if ($this->input->getOption('gzip') and ! function_exists('gzencode')) 90 | { 91 | $this->error('[gzip] Build will not use Gzip as the required dependencies are not available.'); 92 | $this->line(''); 93 | } 94 | 95 | foreach ($collections as $name => $collection) 96 | { 97 | if ($production) 98 | { 99 | $this->buildAsProduction($name, $collection); 100 | } 101 | else 102 | { 103 | $this->buildAsDevelopment($name, $collection); 104 | } 105 | 106 | $this->cleaner->clean($name); 107 | } 108 | } 109 | 110 | /** 111 | * Dynamically handle calls to the build methods. 112 | * 113 | * @param string $method 114 | * @param array $parameters 115 | * @return mixed 116 | */ 117 | public function __call($method, $parameters) 118 | { 119 | if (in_array($method, array('buildAsDevelopment', 'buildAsProduction'))) 120 | { 121 | list($name, $collection) = $parameters; 122 | 123 | try 124 | { 125 | $this->builder->{$method}($collection, 'stylesheets'); 126 | 127 | $this->line('['.$name.'] Stylesheets successfully built.'); 128 | } 129 | catch (BuildNotRequiredException $error) 130 | { 131 | $this->line('['.$name.'] Stylesheets build was not required for collection.'); 132 | } 133 | 134 | try 135 | { 136 | $this->builder->{$method}($collection, 'javascripts'); 137 | 138 | $this->line('['.$name.'] Javascripts successfully built.'); 139 | } 140 | catch (BuildNotRequiredException $error) 141 | { 142 | $this->line('['.$name.'] Javascripts build was not required for collection.'); 143 | } 144 | 145 | $this->line(''); 146 | } 147 | else 148 | { 149 | return parent::__call($method, $parameters); 150 | } 151 | } 152 | 153 | /** 154 | * Gather the collections to be built. 155 | * 156 | * @return array 157 | */ 158 | protected function gatherCollections() 159 | { 160 | if ( ! is_null($collection = $this->input->getArgument('collection'))) 161 | { 162 | if ( ! $this->environment->has($collection)) 163 | { 164 | $this->comment('['.$collection.'] Collection not found.'); 165 | 166 | return array(); 167 | } 168 | 169 | $this->comment('Gathering assets for collection...'); 170 | 171 | $collections = array($collection => $this->environment->collection($collection)); 172 | } 173 | else 174 | { 175 | $this->comment('Gathering all collections to build...'); 176 | 177 | $collections = $this->environment->all(); 178 | } 179 | 180 | $this->line(''); 181 | 182 | return $collections; 183 | } 184 | 185 | /** 186 | * Get the console command arguments. 187 | * 188 | * @return array 189 | */ 190 | protected function getArguments() 191 | { 192 | return array( 193 | array('collection', InputArgument::OPTIONAL, 'The asset collection to build'), 194 | ); 195 | } 196 | 197 | /** 198 | * Get the console command options. 199 | * 200 | * @return array 201 | */ 202 | protected function getOptions() 203 | { 204 | return array( 205 | array('production', 'p', InputOption::VALUE_NONE, 'Build assets for a production environment'), 206 | array('gzip', null, InputOption::VALUE_NONE, 'Gzip built assets'), 207 | array('force', 'f', InputOption::VALUE_NONE, 'Forces a re-build of the collection') 208 | ); 209 | } 210 | 211 | } -------------------------------------------------------------------------------- /tests/Basset/AssetFinderTest.php: -------------------------------------------------------------------------------- 1 | files = m::mock('Illuminate\Filesystem\Filesystem'); 19 | $this->config = m::mock('Illuminate\Config\Repository'); 20 | 21 | $this->finder = new AssetFinder($this->files, $this->config, 'path/to/public'); 22 | } 23 | 24 | 25 | public function testFindRemotelyHostedAsset() 26 | { 27 | $this->config->shouldReceive('get')->once()->with('basset::aliases.assets.http://foo.bar/baz.css', 'http://foo.bar/baz.css')->andReturn('http://foo.bar/baz.css'); 28 | 29 | $this->assertEquals('http://foo.bar/baz.css', $this->finder->find('http://foo.bar/baz.css')); 30 | } 31 | 32 | 33 | public function testFindRelativeProtocolRemotelyHostedAsset() 34 | { 35 | $this->config->shouldReceive('get')->once()->with('basset::aliases.assets.//foo.bar/baz.css', '//foo.bar/baz.css')->andReturn('//foo.bar/baz.css'); 36 | 37 | $this->assertEquals('//foo.bar/baz.css', $this->finder->find('//foo.bar/baz.css')); 38 | } 39 | 40 | 41 | public function testFindPackageAsset() 42 | { 43 | $this->finder->addNamespace('bar', 'foo/bar'); 44 | 45 | $this->config->shouldReceive('get')->once()->with('basset::aliases.assets.bar::baz.css', 'bar::baz.css')->andReturn('bar::baz.css'); 46 | $this->files->shouldReceive('exists')->once()->with('path/to/public/packages/foo/bar/baz.css')->andReturn(true); 47 | 48 | $this->assertEquals('path/to/public/packages/foo/bar/baz.css', $this->finder->find('bar::baz.css')); 49 | } 50 | 51 | 52 | /** 53 | * @expectedException Basset\Exceptions\AssetNotFoundException 54 | */ 55 | public function testFindPackageAssetWithNoSetPackageThrowsNotFoundException() 56 | { 57 | $this->config->shouldReceive('get')->once()->with('basset::aliases.assets.bar::baz.css', 'bar::baz.css')->andReturn('bar::baz.css'); 58 | 59 | $this->files->shouldReceive('exists')->once()->with('path/to/public/bar::baz.css')->andReturn(false); 60 | $this->files->shouldReceive('exists')->once()->with('bar::baz.css')->andReturn(false); 61 | 62 | $this->assertNull($this->finder->find('bar::baz.css')); 63 | } 64 | 65 | 66 | public function testFindWorkingDirectoryAsset() 67 | { 68 | $this->config->shouldReceive('get')->once()->with('basset::aliases.assets.foo.css', 'foo.css')->andReturn('foo.css'); 69 | 70 | $this->files->shouldReceive('exists')->once()->with('path/to/public/working/directory')->andReturn(true); 71 | $this->finder->setWorkingDirectory('working/directory'); 72 | $this->files->shouldReceive('exists')->once()->with('path/to/public/working/directory/foo.css')->andReturn(true); 73 | 74 | $this->assertEquals('path/to/public/working/directory/foo.css', $this->finder->find('foo.css')); 75 | } 76 | 77 | 78 | public function testFindPublicPathAsset() 79 | { 80 | $this->config->shouldReceive('get')->once()->with('basset::aliases.assets.foo.css', 'foo.css')->andReturn('foo.css'); 81 | 82 | $this->files->shouldReceive('exists')->once()->with('path/to/public/foo.css')->andReturn(true); 83 | 84 | $this->assertEquals('path/to/public/foo.css', $this->finder->find('foo.css')); 85 | } 86 | 87 | 88 | public function testFindAbsolutePathAsset() 89 | { 90 | $this->config->shouldReceive('get')->once()->with('basset::aliases.assets./absolute/path/to/foo.css', '/absolute/path/to/foo.css')->andReturn('/absolute/path/to/foo.css'); 91 | 92 | $this->files->shouldReceive('exists')->once()->with('path/to/public/absolute/path/to/foo.css')->andReturn(false); 93 | $this->files->shouldReceive('exists')->once()->with('/absolute/path/to/foo.css')->andReturn(true); 94 | 95 | $this->assertEquals('/absolute/path/to/foo.css', $this->finder->find('/absolute/path/to/foo.css')); 96 | } 97 | 98 | 99 | public function testFindAliasedAsset() 100 | { 101 | $this->config->shouldReceive('get')->once()->with('basset::aliases.assets.foo', 'foo')->andReturn('foo.css'); 102 | 103 | $this->files->shouldReceive('exists')->once()->with('path/to/public/foo.css')->andReturn(true); 104 | 105 | $this->assertEquals('path/to/public/foo.css', $this->finder->find('foo')); 106 | } 107 | 108 | 109 | /** 110 | * @expectedException Basset\Exceptions\DirectoryNotFoundException 111 | */ 112 | public function testSettingInvalidWorkingDirectoryThrowsException() 113 | { 114 | $this->files->shouldReceive('exists')->once()->with('path/to/public/working/directory')->andReturn(false); 115 | $this->finder->setWorkingDirectory('working/directory'); 116 | } 117 | 118 | 119 | public function testResettingWorkingDirectory() 120 | { 121 | $this->files->shouldReceive('exists')->once()->with('path/to/public/working/directory')->andReturn(true); 122 | $this->finder->setWorkingDirectory('working/directory'); 123 | $this->assertEquals('path/to/public/working/directory', $this->finder->getWorkingDirectory()); 124 | 125 | $this->finder->resetWorkingDirectory(); 126 | $this->assertFalse($this->finder->getWorkingDirectory()); 127 | } 128 | 129 | 130 | public function testWorkingDirectoryStackIsPrefixed() 131 | { 132 | $this->files->shouldReceive('exists')->once()->with('path/to/public/working/directory')->andReturn(true); 133 | $this->files->shouldReceive('exists')->once()->with('path/to/public/working/directory/foo/bar/baz')->andReturn(true); 134 | 135 | $this->finder->setWorkingDirectory('working/directory'); 136 | $this->assertEquals('path/to/public/working/directory', $this->finder->getWorkingDirectory()); 137 | 138 | $this->finder->setWorkingDirectory('foo/bar/baz'); 139 | $this->assertEquals('path/to/public/working/directory/foo/bar/baz', $this->finder->getWorkingDirectory()); 140 | 141 | $this->finder->resetWorkingDirectory(); 142 | $this->assertEquals('path/to/public/working/directory', $this->finder->getWorkingDirectory()); 143 | 144 | $this->finder->resetWorkingDirectory(); 145 | $this->assertFalse($this->finder->getWorkingDirectory()); 146 | } 147 | 148 | 149 | } -------------------------------------------------------------------------------- /src/Basset/Filter/UriRewriteFilter.php: -------------------------------------------------------------------------------- 1 | 12 | * @license 13 | * @package Minify 14 | * @copyright 2008 Steve Clay / Ryan Grove 15 | */ 16 | class UriRewriteFilter implements FilterInterface { 17 | 18 | /** 19 | * Applications document root. This is typically the public directory. 20 | * 21 | * @var string 22 | */ 23 | protected $documentRoot; 24 | 25 | /** 26 | * Root directory of the asset. 27 | * 28 | * @var string 29 | */ 30 | protected $assetDirectory; 31 | 32 | /** 33 | * Array of symbolic links. 34 | * 35 | * @var array 36 | */ 37 | protected $symlinks; 38 | 39 | /** 40 | * Create a new UriRewriteFilter instance. 41 | * 42 | * @param string $documentRoot 43 | * @param array $symlinks 44 | * @return void 45 | */ 46 | public function __construct($documentRoot = null, $symlinks = array()) 47 | { 48 | $this->documentRoot = $this->realPath($documentRoot ?: $_SERVER['DOCUMENT_ROOT']); 49 | $this->symlinks = $symlinks; 50 | } 51 | 52 | /** 53 | * Apply filter on file load. 54 | * 55 | * @param \Assetic\Asset\AssetInterface $asset 56 | * @return void 57 | */ 58 | public function filterLoad(AssetInterface $asset){} 59 | 60 | /** 61 | * Apply a filter on file dump. 62 | * 63 | * @param \Assetic\Asset\AssetInterface $asset 64 | * @return void 65 | */ 66 | public function filterDump(AssetInterface $asset) 67 | { 68 | $this->assetDirectory = $this->realPath($asset->getSourceRoot()); 69 | 70 | $content = $asset->getContent(); 71 | 72 | // Spin through the symlinks and normalize them. We'll first unset the original 73 | // symlink so that it doesn't clash with the new symlinks once they are added 74 | // back in. 75 | foreach ($this->symlinks as $link => $target) 76 | { 77 | unset($this->symlinks[$link]); 78 | 79 | if ($link == '//') 80 | { 81 | $link = $this->documentRoot; 82 | } 83 | else 84 | { 85 | $link = str_replace('//', $this->documentRoot.'/', $link); 86 | } 87 | 88 | $link = strtr($link, '/', DIRECTORY_SEPARATOR); 89 | 90 | $this->symlinks[$link] = $this->realPath($target); 91 | } 92 | 93 | $content = $this->trimUrls($content); 94 | 95 | $content = preg_replace_callback('/@import\\s+([\'"])(.*?)[\'"]/', array($this, 'processUriCallback'), $content); 96 | 97 | $content = preg_replace_callback('/url\\(\\s*([^\\)\\s]+)\\s*\\)/', array($this, 'processUriCallback'), $content); 98 | 99 | $asset->setContent($content); 100 | } 101 | 102 | /** 103 | * Takes a path and transforms it to a real path. 104 | * 105 | * @param string $path 106 | * @return string 107 | */ 108 | protected function realPath($path) 109 | { 110 | if ($realPath = realpath($path)) 111 | { 112 | $path = $realPath; 113 | } 114 | 115 | return rtrim($path, '/\\'); 116 | } 117 | 118 | /** 119 | * Trims URLs. 120 | * 121 | * @param string $content 122 | * @return string 123 | */ 124 | protected function trimUrls($content) 125 | { 126 | return preg_replace('/url\\(\\s*([^\\)]+?)\\s*\\)/x', 'url($1)', $content); 127 | } 128 | 129 | /** 130 | * Processes a regular expression callback, determines the URI and returns the rewritten URIs. 131 | * 132 | * @param array $matches 133 | * @return string 134 | */ 135 | protected function processUriCallback($matches) 136 | { 137 | $isImport = $matches[0][0] === '@'; 138 | 139 | // Determine what the quote character and the URI is, if there is one. 140 | $quoteCharacter = $uri = null; 141 | 142 | if ($isImport) 143 | { 144 | $quoteCharater = $matches[1]; 145 | 146 | $uri = $matches[2]; 147 | } 148 | else 149 | { 150 | if ($matches[1][0] === "'" or $matches[1][0] === '"') 151 | { 152 | $quoteCharacter = $matches[1][0]; 153 | } 154 | 155 | if ( ! $quoteCharacter) 156 | { 157 | $uri = $matches[1]; 158 | } 159 | else 160 | { 161 | $uri = substr($matches[1], 1, strlen($matches[1]) - 2); 162 | } 163 | } 164 | 165 | // Analyze the URI 166 | if ($uri[0] !== '/' and strpos($uri, '//') === false and strpos($uri, 'data') !== 0) 167 | { 168 | $uri = $this->rewriteRelative($uri); 169 | } 170 | 171 | if ($isImport) 172 | { 173 | return "@import {$quoteCharacter}{$uri}{$quoteCharacter}"; 174 | } 175 | 176 | return "url({$quoteCharacter}{$uri}{$quoteCharacter})"; 177 | } 178 | 179 | /** 180 | * Rewrites a relative URI. 181 | * 182 | * @param string $uri 183 | * @return string 184 | */ 185 | protected function rewriteRelative($uri) 186 | { 187 | $path = strtr($this->assetDirectory, '/', DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.strtr($uri, '/', DIRECTORY_SEPARATOR); 188 | 189 | foreach ($this->symlinks as $link => $target) 190 | { 191 | if (strpos($path, $target) === 0) 192 | { 193 | $path = $link.substr($path, strlen($target)); 194 | 195 | break; 196 | } 197 | } 198 | 199 | // Strip the document root from the path. 200 | $path = substr($path, strlen($this->documentRoot)); 201 | 202 | $uri = strtr($path, '/\\', '//'); 203 | $uri = $this->removeDots($uri); 204 | 205 | return $uri; 206 | } 207 | 208 | /** 209 | * Removes dots from a URI. 210 | * 211 | * @param string $uri 212 | * @return string 213 | */ 214 | protected function removeDots($uri) 215 | { 216 | $uri = str_replace('/./', '/', $uri); 217 | 218 | do 219 | { 220 | $uri = preg_replace('@/[^/]+/\\.\\./@', '/', $uri, 1, $changed); 221 | } 222 | while ($changed); 223 | 224 | return $uri; 225 | } 226 | 227 | } -------------------------------------------------------------------------------- /src/Basset/Builder/FilesystemCleaner.php: -------------------------------------------------------------------------------- 1 | environment = $environment; 51 | $this->manifest = $manifest; 52 | $this->files = $files; 53 | $this->buildPath = $buildPath; 54 | } 55 | 56 | /** 57 | * Clean all built collections and the manifest entries. 58 | * 59 | * @return void 60 | */ 61 | public function cleanAll() 62 | { 63 | $collections = array_keys($this->environment->all()) + array_keys($this->manifest->all()); 64 | 65 | foreach ($collections as $collection) 66 | { 67 | $this->clean($collection); 68 | } 69 | } 70 | 71 | /** 72 | * Cleans a built collection and the manifest entries. 73 | * 74 | * @param string $collection 75 | * @return void 76 | */ 77 | public function clean($collection) 78 | { 79 | $entry = $this->manifest->get($collection); 80 | 81 | // If the collection exists on the environment then we'll proceed with cleaning the filesystem 82 | // This removes any double-up production and development builds. 83 | if (isset($this->environment[$collection])) 84 | { 85 | $this->cleanFilesystem($this->environment[$collection], $entry); 86 | } 87 | 88 | // If the collection does not exist on the environment then we'll instrcut the manifest to 89 | // forget this collection. 90 | else 91 | { 92 | $this->manifest->forget($collection); 93 | } 94 | 95 | // Cleaning the manifest is important as it will also remove unnecessary files from the 96 | // filesystem if a collection has been removed. 97 | $this->cleanManifestFiles($collection, $entry); 98 | 99 | $this->manifest->save(); 100 | } 101 | 102 | /** 103 | * Cleans a built collections files removing any outdated builds. 104 | * 105 | * @param \Basset\Collection $collection 106 | * @param \Basset\Manifest\Entry $entry 107 | * @return void 108 | */ 109 | protected function cleanFilesystem(Collection $collection, Entry $entry) 110 | { 111 | $this->cleanProductionFiles($collection, $entry); 112 | 113 | $this->cleanDevelopmentFiles($collection, $entry); 114 | } 115 | 116 | /** 117 | * Clean the collections manifest entry files. 118 | * 119 | * @param string $collection 120 | * @param \Basset\Manifest\Entry $entry 121 | * @return void 122 | */ 123 | protected function cleanManifestFiles($collection, Entry $entry) 124 | { 125 | if ( ! $entry->hasProductionFingerprints() or ! isset($this->environment[$collection])) 126 | { 127 | $this->deleteMatchingFiles($this->buildPath.'/'.$collection.'-*.*'); 128 | 129 | $entry->resetProductionFingerprints(); 130 | } 131 | 132 | if ( ! $entry->hasDevelopmentAssets() or ! isset($this->environment[$collection])) 133 | { 134 | $this->files->deleteDirectory($this->buildPath.'/'.$collection); 135 | 136 | $entry->resetDevelopmentAssets(); 137 | } 138 | } 139 | 140 | /** 141 | * Clean collection production files. 142 | * 143 | * @param \Basset\Collection $collection 144 | * @param \Basset\Manifest\Entry $entry 145 | * @return void 146 | */ 147 | protected function cleanProductionFiles(Collection $collection, Entry $entry) 148 | { 149 | foreach ($entry->getProductionFingerprints() as $fingerprint) 150 | { 151 | $wildcardPath = $this->replaceFingerprintWithWildcard($fingerprint); 152 | 153 | $this->deleteMatchingFiles($this->buildPath.'/'.$wildcardPath, $fingerprint); 154 | } 155 | } 156 | 157 | /** 158 | * Clean collection development files. 159 | * 160 | * @param \Basset\Collection $collection 161 | * @param \Basset\Manifest\Entry $entry 162 | * @return void 163 | */ 164 | protected function cleanDevelopmentFiles(Collection $collection, Entry $entry) 165 | { 166 | foreach ($entry->getDevelopmentAssets() as $assets) 167 | { 168 | foreach ($assets as $asset) 169 | { 170 | $wildcardPath = $this->replaceFingerprintWithWildcard($asset); 171 | 172 | $this->deleteMatchingFiles($this->buildPath.'/'.$collection->getIdentifier().'/'.$wildcardPath, array_values($assets)); 173 | } 174 | } 175 | } 176 | 177 | /** 178 | * Delete matching files from the wildcard glob search except the ignored file. 179 | * 180 | * @param string $wildcard 181 | * @param array|string $ignored 182 | * @return void 183 | */ 184 | protected function deleteMatchingFiles($wildcard, $ignored = null) 185 | { 186 | if (is_array($files = $this->files->glob($wildcard))) 187 | { 188 | foreach ($files as $path) 189 | { 190 | if ( ! is_null($ignored)) 191 | { 192 | // Spin through each of the ignored assets and if the current file path ends 193 | // with any of the ignored asset paths then we'll skip this asset as it 194 | // needs to be kept. 195 | foreach ((array) $ignored as $ignore) 196 | { 197 | if (ends_with($path, $ignore)) continue 2; 198 | } 199 | } 200 | 201 | $this->files->delete($path); 202 | } 203 | } 204 | 205 | } 206 | 207 | /** 208 | * Replace a fingerprint with a wildcard. 209 | * 210 | * @param string $value 211 | * @return string 212 | */ 213 | protected function replaceFingerprintWithWildcard($value) 214 | { 215 | return preg_replace('/(.*?)-([\w\d]{32})\.(.*?)/', '$1-*.$3', $value); 216 | } 217 | 218 | } -------------------------------------------------------------------------------- /src/Basset/AssetFinder.php: -------------------------------------------------------------------------------- 1 | files = $files; 50 | $this->config = $config; 51 | $this->publicPath = $publicPath; 52 | } 53 | 54 | /** 55 | * Find and return an assets path. 56 | * 57 | * @param string $name 58 | * @return string 59 | * @throws \Basset\Exceptions\AssetExistsException 60 | * @throws \Basset\Exceptions\AssetNotFoundException 61 | */ 62 | public function find($name) 63 | { 64 | $name = $this->config->get("basset::aliases.assets.{$name}", $name); 65 | 66 | // Spin through an array of methods ordered by the priority of how an asset should be found. 67 | // Once we find a non-null path we'll return that path breaking from the loop. 68 | foreach (array('RemotelyHosted', 'PackageAsset', 'WorkingDirectory', 'PublicPath', 'AbsolutePath') as $method) 69 | { 70 | if ($path = $this->{'find'.$method}($name)) 71 | { 72 | return $path; 73 | } 74 | } 75 | 76 | throw new AssetNotFoundException; 77 | } 78 | 79 | /** 80 | * Find a remotely hosted asset. 81 | * 82 | * @param string $name 83 | * @return null|string 84 | */ 85 | public function findRemotelyHosted($name) 86 | { 87 | if (filter_var($name, FILTER_VALIDATE_URL) or starts_with($name, '//')) 88 | { 89 | return $name; 90 | } 91 | } 92 | 93 | /** 94 | * Find an asset by looking for a prefixed package namespace. 95 | * 96 | * @param string $name 97 | * @return null|string 98 | */ 99 | public function findPackageAsset($name) 100 | { 101 | if (str_contains($name, '::')) 102 | { 103 | list($namespace, $name) = explode('::', $name); 104 | 105 | if ( ! isset($this->hints[$namespace])) 106 | { 107 | return; 108 | } 109 | 110 | $path = $this->prefixPublicPath('packages/'.$this->hints[$namespace].'/'.$name); 111 | 112 | if ($this->files->exists($path)) 113 | { 114 | return $path; 115 | } 116 | } 117 | } 118 | 119 | /** 120 | * Find an asset by searching in the current working directory. 121 | * 122 | * @param string $name 123 | * @return null|string 124 | */ 125 | public function findWorkingDirectory($name) 126 | { 127 | $path = $this->getWorkingDirectory().'/'.$name; 128 | 129 | if ($this->withinWorkingDirectory() and $this->files->exists($path)) 130 | { 131 | return $path; 132 | } 133 | } 134 | 135 | /** 136 | * Find an asset by searching in the public path. 137 | * 138 | * @param string $name 139 | * @return null|string 140 | */ 141 | public function findPublicPath($name) 142 | { 143 | $path = $this->prefixPublicPath($name); 144 | 145 | if ($this->files->exists($path)) 146 | { 147 | return $path; 148 | } 149 | } 150 | 151 | /** 152 | * Find an asset by absolute path. 153 | * 154 | * @param string $name 155 | * @return null|string 156 | */ 157 | public function findAbsolutePath($name) 158 | { 159 | if ($this->files->exists($name)) 160 | { 161 | return $name; 162 | } 163 | } 164 | 165 | /** 166 | * Set the working directory path. 167 | * 168 | * @param string $path 169 | * @return string 170 | * @throws \Basset\Exceptions\DirectoryNotFoundException 171 | */ 172 | public function setWorkingDirectory($path) 173 | { 174 | $path = $this->prefixDirectoryStack($path); 175 | 176 | if ($this->files->exists($path)) 177 | { 178 | return $this->directoryStack[] = $path; 179 | } 180 | 181 | throw new DirectoryNotFoundException("Directory [{$path}] could not be found."); 182 | } 183 | 184 | /** 185 | * Pop the last directory from the directory stack. 186 | * 187 | * @return string 188 | */ 189 | public function resetWorkingDirectory() 190 | { 191 | return array_pop($this->directoryStack); 192 | } 193 | 194 | /** 195 | * Determine if within a working directory. 196 | * 197 | * @return bool 198 | */ 199 | public function withinWorkingDirectory() 200 | { 201 | return ! empty($this->directoryStack); 202 | } 203 | 204 | /** 205 | * Get the last working directory path off the directory stack. 206 | * 207 | * @return string 208 | */ 209 | public function getWorkingDirectory() 210 | { 211 | return end($this->directoryStack); 212 | } 213 | 214 | /** 215 | * Get the working directory stack. 216 | * 217 | * @return array 218 | */ 219 | public function getDirectoryStack() 220 | { 221 | return $this->directoryStack; 222 | } 223 | 224 | /** 225 | * Prefix the last directory from the stack or the public path if not 226 | * within a working directory 227 | * 228 | * @param string $path 229 | * @return string 230 | */ 231 | public function prefixDirectoryStack($path) 232 | { 233 | if ($this->withinWorkingDirectory()) 234 | { 235 | return rtrim($this->getWorkingDirectory().'/'.ltrim($path, '/'), '/'); 236 | } 237 | 238 | return $this->prefixPublicPath($path); 239 | } 240 | 241 | /** 242 | * Add a package namespace. 243 | * 244 | * @param string $namespace 245 | * @param string $package 246 | * @return void 247 | */ 248 | public function addNamespace($namespace, $package) 249 | { 250 | $this->hints[$namespace] = $package; 251 | } 252 | 253 | /** 254 | * Prefix the public path to a path. 255 | * 256 | * @param string $path 257 | * @return string 258 | */ 259 | protected function prefixPublicPath($path) 260 | { 261 | return rtrim($this->publicPath.'/'.ltrim($path, '/'), '/'); 262 | } 263 | 264 | /** 265 | * Get the public path. 266 | * 267 | * @return string 268 | */ 269 | public function getPublicPath() 270 | { 271 | return $this->publicPath; 272 | } 273 | 274 | } -------------------------------------------------------------------------------- /src/Basset/BassetServiceProvider.php: -------------------------------------------------------------------------------- 1 | package('jasonlewis/basset', 'basset', __DIR__.'/../'); 54 | 55 | // Tell the logger to use a rotating files setup to log problems encountered during 56 | // Bassets operation but only when debugging is enabled. 57 | if ($this->app['config']->get('basset::debug', false)) 58 | { 59 | $this->app['basset.log']->useDailyFiles($this->app['path.storage'].'/logs/basset.txt', 0, 'warning'); 60 | } 61 | 62 | // If debugging is disabled we'll use a null handler to essentially send all logged 63 | // messages into a blackhole. 64 | else 65 | { 66 | $handler = new NullHandler(MonologLogger::WARNING); 67 | 68 | $this->app['basset.log']->getMonolog()->pushHandler($handler); 69 | } 70 | 71 | $this->app->instance('basset.path.build', $this->app['path.public'].'/'.$this->app['config']->get('basset::build_path')); 72 | 73 | $this->registerBladeExtensions(); 74 | 75 | // Collections can be defined as an array in the configuration file. We'll register 76 | // this array of collections with the environment. 77 | $this->app['basset']->collections($this->app['config']->get('basset::collections')); 78 | 79 | // Load the local manifest that contains the fingerprinted paths to both production 80 | // and development builds. 81 | $this->app['basset.manifest']->load(); 82 | } 83 | 84 | /** 85 | * Register the Blade extensions with the compiler. 86 | * 87 | * @return void 88 | */ 89 | protected function registerBladeExtensions() 90 | { 91 | $blade = $this->app['view']->getEngineResolver()->resolve('blade')->getCompiler(); 92 | 93 | $blade->extend(function($value, $compiler) 94 | { 95 | $matcher = $compiler->createMatcher('javascripts'); 96 | 97 | return preg_replace($matcher, '$1', $value); 98 | }); 99 | 100 | $blade->extend(function($value, $compiler) 101 | { 102 | $matcher = $compiler->createMatcher('stylesheets'); 103 | 104 | return preg_replace($matcher, '$1', $value); 105 | }); 106 | 107 | $blade->extend(function($value, $compiler) 108 | { 109 | $matcher = $compiler->createMatcher('assets'); 110 | 111 | return preg_replace($matcher, '$1', $value); 112 | }); 113 | } 114 | 115 | /** 116 | * Register the service provider. 117 | * 118 | * @return void 119 | */ 120 | public function register() 121 | { 122 | foreach ($this->components as $component) 123 | { 124 | $this->{'register'.$component}(); 125 | } 126 | } 127 | 128 | /** 129 | * Register the asset finder. 130 | * 131 | * @return void 132 | */ 133 | protected function registerAssetFinder() 134 | { 135 | $this->app['basset.finder'] = $this->app->share(function($app) 136 | { 137 | return new AssetFinder($app['files'], $app['config'], $app['path.public']); 138 | }); 139 | } 140 | 141 | /** 142 | * Register the collection server. 143 | * 144 | * @return void 145 | */ 146 | protected function registerServer() 147 | { 148 | $this->app['basset.server'] = $this->app->share(function($app) 149 | { 150 | return new Server($app); 151 | }); 152 | } 153 | 154 | /** 155 | * Register the logger. 156 | * 157 | * @return void 158 | */ 159 | protected function registerLogger() 160 | { 161 | $this->app['basset.log'] = $this->app->share(function($app) 162 | { 163 | return new Writer(new \Monolog\Logger('basset'), $app['events']); 164 | }); 165 | } 166 | 167 | /** 168 | * Register the factory manager. 169 | * 170 | * @return void 171 | */ 172 | protected function registerFactoryManager() 173 | { 174 | $this->app['basset.factory'] = $this->app->share(function($app) 175 | { 176 | return new FactoryManager($app); 177 | }); 178 | } 179 | 180 | /** 181 | * Register the collection repository. 182 | * 183 | * @return void 184 | */ 185 | protected function registerManifest() 186 | { 187 | $this->app['basset.manifest'] = $this->app->share(function($app) 188 | { 189 | $meta = $app['config']->get('app.manifest'); 190 | 191 | return new Manifest($app['files'], $meta); 192 | }); 193 | } 194 | 195 | /** 196 | * Register the collection builder. 197 | * 198 | * @return void 199 | */ 200 | protected function registerBuilder() 201 | { 202 | $this->app['basset.builder'] = $this->app->share(function($app) 203 | { 204 | return new Builder($app['files'], $app['basset.manifest'], $app['basset.path.build']); 205 | }); 206 | 207 | $this->app['basset.builder.cleaner'] = $this->app->share(function($app) 208 | { 209 | return new FilesystemCleaner($app['basset'], $app['basset.manifest'], $app['files'], $app['basset.path.build']); 210 | }); 211 | } 212 | 213 | /** 214 | * Register the basset environment. 215 | * 216 | * @return void 217 | */ 218 | protected function registerBasset() 219 | { 220 | $this->app['basset'] = $this->app->share(function($app) 221 | { 222 | return new Environment($app['basset.factory'], $app['basset.finder']); 223 | }); 224 | } 225 | 226 | /** 227 | * Register the commands. 228 | * 229 | * @return void 230 | */ 231 | public function registerCommands() 232 | { 233 | $this->registerBassetCommand(); 234 | 235 | $this->registerBuildCommand(); 236 | 237 | $this->commands('command.basset', 'command.basset.build'); 238 | } 239 | 240 | /** 241 | * Register the basset command. 242 | * 243 | * @return void 244 | */ 245 | protected function registerBassetCommand() 246 | { 247 | $this->app['command.basset'] = $this->app->share(function($app) 248 | { 249 | return new BassetCommand($app['basset.manifest'], $app['basset'], $app['basset.builder.cleaner']); 250 | }); 251 | } 252 | 253 | /** 254 | * Register the build command. 255 | * 256 | * @return void 257 | */ 258 | protected function registerBuildCommand() 259 | { 260 | $this->app['command.basset.build'] = $this->app->share(function($app) 261 | { 262 | return new BuildCommand($app['basset'], $app['basset.builder'], $app['basset.builder.cleaner']); 263 | }); 264 | } 265 | 266 | } -------------------------------------------------------------------------------- /src/Basset/Builder/Builder.php: -------------------------------------------------------------------------------- 1 | files = $files; 57 | $this->manifest = $manifest; 58 | $this->buildPath = $buildPath; 59 | 60 | $this->makeBuildPath(); 61 | } 62 | 63 | /** 64 | * Build a production collection. 65 | * 66 | * @param \Basset\Collection $collection 67 | * @param string $group 68 | * @return void 69 | * @throws \Basset\Exceptions\BuildNotRequiredException 70 | */ 71 | public function buildAsProduction(Collection $collection, $group) 72 | { 73 | // Get the assets of the given group from the collection. The collection is also responsible 74 | // for handling any ordering of the assets so that we just need to build them. 75 | $assets = $collection->getAssetsWithoutRaw($group); 76 | 77 | $entry = $this->manifest->make($identifier = $collection->getIdentifier()); 78 | 79 | // Build the assets and transform the array into a newline separated string. We'll use this 80 | // as a basis for the collections fingerprint and it will decide as to whether the 81 | // collection needs to be rebuilt. 82 | $build = array_to_newlines($assets->map(function($asset) { return $asset->build(true); })->all()); 83 | 84 | // If the build is empty then we'll reset the fingerprint on the manifest entry and throw the 85 | // exception as there's no point going any further. 86 | if (empty($build)) 87 | { 88 | $entry->resetProductionFingerprint($group); 89 | 90 | throw new BuildNotRequiredException; 91 | } 92 | 93 | $fingerprint = $identifier.'-'.md5($build).'.'.$collection->getExtension($group); 94 | 95 | $path = $this->buildPath.'/'.$fingerprint; 96 | 97 | // If the collection has already been built and we're not forcing the build then we'll throw 98 | // the exception here as we don't need to rebuild the collection. 99 | if ($fingerprint == $entry->getProductionFingerprint($group) and ! $this->force and $this->files->exists($path)) 100 | { 101 | throw new BuildNotRequiredException; 102 | } 103 | else 104 | { 105 | $this->files->put($path, $this->gzip($build)); 106 | 107 | $entry->setProductionFingerprint($group, $fingerprint); 108 | } 109 | } 110 | 111 | /** 112 | * Build a development collection. 113 | * 114 | * @param \Basset\Collection $collection 115 | * @param string $group 116 | * @return void 117 | * @throws \Basset\Exceptions\BuildNotRequiredException 118 | */ 119 | public function buildAsDevelopment(Collection $collection, $group) 120 | { 121 | // Get the assets of the given group from the collection. The collection is also responsible 122 | // for handling any ordering of the assets so that we just need to build them. 123 | $assets = $collection->getAssetsWithoutRaw($group); 124 | 125 | $entry = $this->manifest->make($identifier = $collection->getIdentifier()); 126 | 127 | // If the collection definition has changed when compared to the manifest entry or if the 128 | // collection is being forcefully rebuilt then we'll reset the development assets. 129 | if ($this->collectionDefinitionHasChanged($assets, $entry, $group) or $this->force) 130 | { 131 | $entry->resetDevelopmentAssets($group); 132 | } 133 | 134 | // Otherwise we'll look at each of the assets and see if the entry has the asset or if 135 | // the assets build path differs from that of the manifest entry. 136 | else 137 | { 138 | $assets = $assets->filter(function($asset) use ($entry) 139 | { 140 | return ! $entry->hasDevelopmentAsset($asset) or $asset->getBuildPath() != $entry->getDevelopmentAsset($asset); 141 | }); 142 | } 143 | 144 | if ( ! $assets->isEmpty()) 145 | { 146 | foreach ($assets as $asset) 147 | { 148 | $path = "{$this->buildPath}/{$identifier}/{$asset->getBuildPath()}"; 149 | 150 | // If the build directory does not exist we'll attempt to recursively create it so we can 151 | // build the asset to the directory. 152 | ! $this->files->exists($directory = dirname($path)) and $this->files->makeDirectory($directory, 0777, true); 153 | 154 | $this->files->put($path, $this->gzip($asset->build())); 155 | 156 | // Add the development asset to the manifest entry so that we can save the built asset 157 | // to the manifest. 158 | $entry->addDevelopmentAsset($asset); 159 | } 160 | } 161 | else 162 | { 163 | throw new BuildNotRequiredException; 164 | } 165 | } 166 | 167 | /** 168 | * Determine if the collections definition has changed when compared to the manifest. 169 | * 170 | * @param \Illuminate\Support\Collection $assets 171 | * @param \Basset\Manifest\Entry $entry 172 | * @param string $group 173 | * @return bool 174 | */ 175 | protected function collectionDefinitionHasChanged($assets, $entry, $group) 176 | { 177 | // If the manifest entry doesn't even have the group registered then it's obvious that the 178 | // collection has changed and needs to be rebuilt. 179 | if ( ! $entry->hasDevelopmentAssets($group)) 180 | { 181 | return true; 182 | } 183 | 184 | // Get the development assets from the manifest entry and flatten the keys so that we have 185 | // an array of relative paths that we can compare from. 186 | $manifest = $entry->getDevelopmentAssets($group); 187 | 188 | $manifest = array_flatten(array_keys($manifest)); 189 | 190 | // Compute the difference between the collections assets and the manifests assets. If we get 191 | // an array of values then the collection has changed since the last build and everything 192 | // should be rebuilt. 193 | $difference = array_diff_assoc($manifest, $assets->map(function($asset) { return $asset->getRelativePath(); })->flatten()->toArray()); 194 | 195 | return ! empty($difference); 196 | } 197 | 198 | /** 199 | * Make the build path if it does not exist. 200 | * 201 | * @return void 202 | */ 203 | protected function makeBuildPath() 204 | { 205 | if ( ! $this->files->exists($this->buildPath)) 206 | { 207 | $this->files->makeDirectory($this->buildPath); 208 | } 209 | } 210 | 211 | /** 212 | * If Gzipping is enabled the the zlib extension is loaded we'll Gzip the contents 213 | * with a maximum compression level of 9. 214 | * 215 | * @param string $contents 216 | * @return string 217 | */ 218 | protected function gzip($contents) 219 | { 220 | if ($this->gzip and function_exists('gzencode')) 221 | { 222 | return gzencode($contents, 9); 223 | } 224 | 225 | return $contents; 226 | } 227 | 228 | /** 229 | * Set built collections to be gzipped. 230 | * 231 | * @param bool $gzip 232 | * @return \Basset\Builder\Builder 233 | */ 234 | public function setGzip($gzip) 235 | { 236 | $this->gzip = $gzip; 237 | 238 | return $this; 239 | } 240 | 241 | /** 242 | * Set the building to be forced. 243 | * 244 | * @param bool $force 245 | * @return \Basset\Builder\Builder 246 | */ 247 | public function setForce($force) 248 | { 249 | $this->force = $force; 250 | 251 | return $this; 252 | } 253 | 254 | } 255 | -------------------------------------------------------------------------------- /src/Basset/Server.php: -------------------------------------------------------------------------------- 1 | app = $app; 26 | } 27 | 28 | /** 29 | * Serve a collection where the group is determined by the the extension. 30 | * 31 | * @param string $collection 32 | * @param string $format 33 | * @return string 34 | */ 35 | public function collection($collection, $format = null) 36 | { 37 | list($collection, $extension) = preg_split('/\.(css|js)/', $collection, 2, PREG_SPLIT_DELIM_CAPTURE); 38 | 39 | $group = $extension == 'css' ? 'stylesheets' : 'javascripts'; 40 | 41 | return $this->serve($collection, $group, $format); 42 | } 43 | 44 | /** 45 | * Serve the stylesheets for a given collection. 46 | * 47 | * @param string $collection 48 | * @param string $format 49 | * @return string 50 | */ 51 | public function stylesheets($collection, $format = null) 52 | { 53 | return $this->serve($collection, 'stylesheets', $format); 54 | } 55 | 56 | /** 57 | * Serve the javascripts for a given collection. 58 | * 59 | * @param string $collection 60 | * @param string $format 61 | * @return string 62 | */ 63 | public function javascripts($collection, $format = null) 64 | { 65 | return $this->serve($collection, 'javascripts', $format); 66 | } 67 | 68 | /** 69 | * Serve a given group for a collection. 70 | * 71 | * @param string $collection 72 | * @param string $group 73 | * @param string $format 74 | * @return string 75 | */ 76 | public function serve($collection, $group, $format = null) 77 | { 78 | if ( ! isset($this->app['basset'][$collection])) 79 | { 80 | return ''; 81 | } 82 | 83 | // Get the collection instance from the array of collections. This instance will be used 84 | // throughout the building process to fetch assets and compare against the stored 85 | // manfiest of fingerprints. 86 | $collection = $this->app['basset'][$collection]; 87 | 88 | if ($this->runningInProduction() and $this->app['basset.manifest']->has($collection)) 89 | { 90 | if ($this->app['basset.manifest']->get($collection)->hasProductionFingerprint($group)) 91 | { 92 | return $this->serveProductionCollection($collection, $group, $format); 93 | } 94 | } 95 | 96 | return $this->serveDevelopmentCollection($collection, $group, $format); 97 | } 98 | 99 | /** 100 | * Serve a production collection. 101 | * 102 | * @param \Basset\Collection $collection 103 | * @param string $group 104 | * @param string $format 105 | * @return array 106 | */ 107 | protected function serveProductionCollection(Collection $collection, $group, $format) 108 | { 109 | $entry = $this->getCollectionEntry($collection); 110 | 111 | $fingerprint = $entry->getProductionFingerprint($group); 112 | 113 | $production = $this->{'create'.studly_case($group).'Element'}($this->prefixBuildPath($fingerprint), $format); 114 | 115 | return $this->formatResponse($this->serveRawAssets($collection, $group, $format), $production); 116 | } 117 | 118 | /** 119 | * Serve a development collection. 120 | * 121 | * @param \Basset\Collection $collection 122 | * @param string $group 123 | * @param string $format 124 | * @return array 125 | */ 126 | protected function serveDevelopmentCollection(Collection $collection, $group, $format) 127 | { 128 | $identifier = $collection->getIdentifier(); 129 | 130 | // Before we fetch the collections manifest entry we'll try to build the collection 131 | // again if there is anything outstanding. This doesn't have a huge impact on 132 | // page loads time like trying to dynamically serve each asset. 133 | $this->tryDevelopmentBuild($collection, $group); 134 | 135 | $entry = $this->getCollectionEntry($collection); 136 | 137 | $responses = array(); 138 | 139 | foreach ($collection->getAssetsWithRaw($group) as $asset) 140 | { 141 | if ( ! $asset->isRaw() and $path = $entry->getDevelopmentAsset($asset)) 142 | { 143 | $path = $this->prefixBuildPath($identifier.'/'.$path); 144 | } 145 | else 146 | { 147 | $path = $asset->getRelativePath(); 148 | } 149 | 150 | $responses[] = $this->{'create'.studly_case($group).'Element'}($path, $format); 151 | } 152 | 153 | return $this->formatResponse($responses); 154 | } 155 | 156 | /** 157 | * Serve a collections raw assets. 158 | * 159 | * @param \Basset\Collection $collection 160 | * @param string $group 161 | * @param string $format 162 | * @return array 163 | */ 164 | protected function serveRawAssets(Collection $collection, $group, $format) 165 | { 166 | $responses = array(); 167 | 168 | foreach ($collection->getAssetsOnlyRaw($group) as $asset) 169 | { 170 | $path = $asset->getRelativePath(); 171 | 172 | $responses[] = $this->{'create'.studly_case($group).'Element'}($path, $format); 173 | } 174 | 175 | return $responses; 176 | } 177 | 178 | /** 179 | * Format an array of responses and return a string. 180 | * 181 | * @param mixed $args 182 | * @return string 183 | */ 184 | protected function formatResponse() 185 | { 186 | $responses = array(); 187 | 188 | foreach (func_get_args() as $response) 189 | { 190 | $responses = array_merge($responses, (array) $response); 191 | } 192 | 193 | return array_to_newlines($responses); 194 | } 195 | 196 | /** 197 | * Get a collection manifest entry. 198 | * 199 | * @param \Basset\Collection $collection 200 | * @return \Basset\Manifest\Entry 201 | */ 202 | protected function getCollectionEntry(Collection $collection) 203 | { 204 | return $this->app['basset.manifest']->get($collection); 205 | } 206 | 207 | /** 208 | * Try the development build of a collection. 209 | * 210 | * @param \Basset\Collection $collection 211 | * @param string $group 212 | * @return void 213 | */ 214 | protected function tryDevelopmentBuild(Collection $collection, $group) 215 | { 216 | try 217 | { 218 | $this->app['basset.builder']->buildAsDevelopment($collection, $group); 219 | } 220 | catch (BuildNotRequiredException $e) {} 221 | 222 | $this->app['basset.builder.cleaner']->clean($collection->getIdentifier()); 223 | } 224 | 225 | /** 226 | * Prefix the build path to a given path. 227 | * 228 | * @param string $path 229 | * @return string 230 | */ 231 | protected function prefixBuildPath($path) 232 | { 233 | if ($buildPath = $this->app['config']->get('basset::build_path')) 234 | { 235 | $path = "{$buildPath}/{$path}"; 236 | } 237 | 238 | return $path; 239 | } 240 | 241 | /** 242 | * Determine if the application is running in production mode. 243 | * 244 | * @return bool 245 | */ 246 | protected function runningInProduction() 247 | { 248 | return in_array($this->app['env'], (array) $this->app['config']->get('basset::production')); 249 | } 250 | 251 | /** 252 | * Create a stylesheets element for the specified path. 253 | * 254 | * @param string $path 255 | * @param string $format 256 | * @return string 257 | */ 258 | protected function createStylesheetsElement($path, $format) 259 | { 260 | return sprintf($format ?: '', $this->buildAssetUrl($path)); 261 | } 262 | 263 | /** 264 | * Create a javascripts element for the specified path. 265 | * 266 | * @param string $path 267 | * @param string $format 268 | * @return string 269 | */ 270 | protected function createJavascriptsElement($path, $format) 271 | { 272 | return sprintf($format ?: '', $this->buildAssetUrl($path)); 273 | } 274 | 275 | /** 276 | * Build the URL to an asset. 277 | * 278 | * @param string $path 279 | * @return string 280 | */ 281 | public function buildAssetUrl($path) 282 | { 283 | return starts_with($path, '//') ? $path : $this->app['url']->asset($path); 284 | } 285 | 286 | } 287 | -------------------------------------------------------------------------------- /tests/Basset/AssetTest.php: -------------------------------------------------------------------------------- 1 | files = m::mock('Illuminate\Filesystem\Filesystem'); 19 | $this->factory = m::mock('Basset\Factory\FactoryManager'); 20 | $this->log = m::mock('Illuminate\Log\Writer'); 21 | $this->filter = m::mock('Basset\Factory\FilterFactory', array(array(), array(), 'testing'))->shouldDeferMissing(); 22 | 23 | $this->factory->shouldReceive('get')->with('filter')->andReturn($this->filter); 24 | 25 | $this->files->shouldReceive('lastModified')->with('path/to/public/foo/bar.sass')->andReturn('1368422603'); 26 | 27 | $this->asset = new Asset($this->files, $this->factory, 'testing', 'path/to/public/foo/bar.sass', 'foo/bar.sass'); 28 | $this->asset->setOrder(1); 29 | $this->asset->setGroup('stylesheets'); 30 | } 31 | 32 | 33 | public function testGetAssetProperties() 34 | { 35 | $this->assertEquals('foo/bar.sass', $this->asset->getRelativePath()); 36 | $this->assertEquals('path/to/public/foo/bar.sass', $this->asset->getAbsolutePath()); 37 | $this->assertEquals('foo/bar-2a4bdbebcbf798cb0b59078d98136e3d.css', $this->asset->getBuildPath()); 38 | $this->assertEquals('css', $this->asset->getBuildExtension()); 39 | $this->assertInstanceOf('Illuminate\Support\Collection', $this->asset->getFilters()); 40 | $this->assertEquals('stylesheets', $this->asset->getGroup()); 41 | $this->assertEquals('1368422603', $this->asset->getLastModified()); 42 | } 43 | 44 | 45 | public function testAssetsCanBeServedRaw() 46 | { 47 | $this->asset->raw(); 48 | $this->assertTrue($this->asset->isRaw()); 49 | } 50 | 51 | 52 | public function testCheckingOfAssetGroup() 53 | { 54 | $this->assertTrue($this->asset->isStylesheet()); 55 | $this->assertFalse($this->asset->isJavascript()); 56 | } 57 | 58 | 59 | public function testCheckingOfAssetGroupWhenNoGroupSupplied() 60 | { 61 | $this->asset->setGroup(null); 62 | $this->assertTrue($this->asset->isStylesheet()); 63 | } 64 | 65 | 66 | public function testAssetCanBeRemotelyHosted() 67 | { 68 | $asset = new Asset($this->files, $this->factory, 'testing', 'http://foo.com/bar.css', 'http://foo.com/bar.css'); 69 | 70 | $this->assertTrue($asset->isRemote()); 71 | } 72 | 73 | 74 | public function testAssetCanBeRemotelyHostedWithRelativeProtocol() 75 | { 76 | $asset = new Asset($this->files, $this->factory, 'testing', '//foo.com/bar.css', '//foo.com/bar.css'); 77 | 78 | $this->assertTrue($asset->isRemote()); 79 | } 80 | 81 | 82 | public function testSettingCustomOrderOfAsset() 83 | { 84 | $this->asset->first(); 85 | $this->assertEquals(1, $this->asset->getOrder()); 86 | 87 | $this->asset->second(); 88 | $this->assertEquals(2, $this->asset->getOrder()); 89 | 90 | $this->asset->third(); 91 | $this->assertEquals(3, $this->asset->getOrder()); 92 | 93 | $this->asset->order(10); 94 | $this->assertEquals(10, $this->asset->getOrder()); 95 | } 96 | 97 | 98 | public function testFiltersAreAppliedToAssets() 99 | { 100 | $this->filter->shouldReceive('make')->once()->with('FooFilter')->andReturn($filter = m::mock('Basset\Filter\Filter')); 101 | 102 | $filter->shouldReceive('setResource')->once()->with($this->asset)->andReturn(m::self()); 103 | $filter->shouldReceive('getFilter')->once()->andReturn('FooFilter'); 104 | 105 | $this->asset->apply('FooFilter'); 106 | 107 | $filters = $this->asset->getFilters(); 108 | 109 | $this->assertArrayHasKey('FooFilter', $filters->all()); 110 | $this->assertInstanceOf('Basset\Filter\Filter', $filters['FooFilter']); 111 | } 112 | 113 | 114 | public function testArrayOfFiltersAreAppliedToAssets() 115 | { 116 | $this->filter->shouldReceive('make')->once()->with('FooFilter')->andReturn($filter = m::mock('Basset\Filter\Filter')); 117 | $filter->shouldReceive('setResource')->once()->with($this->asset)->andReturn(m::self()); 118 | $filter->shouldReceive('getFilter')->once()->andReturn('FooFilter'); 119 | 120 | $this->filter->shouldReceive('make')->once()->with('BarFilter')->andReturn($filter = m::mock('Basset\Filter\Filter')); 121 | $filter->shouldReceive('setResource')->once()->with($this->asset)->andReturn(m::self()); 122 | $filter->shouldReceive('getFilter')->once()->andReturn('BarFilter'); 123 | 124 | $this->asset->apply(array('FooFilter', 'BarFilter')); 125 | 126 | $filters = $this->asset->getFilters(); 127 | 128 | $this->assertArrayHasKey('FooFilter', $filters->all()); 129 | $this->assertArrayHasKey('BarFilter', $filters->all()); 130 | } 131 | 132 | 133 | public function testArrayOfFiltersWithCallbacksAreAppliedToAssets() 134 | { 135 | $this->filter->shouldReceive('make')->once()->with('FooFilter')->andReturn($filter = m::mock('Basset\Filter\Filter')); 136 | $filter->shouldReceive('setResource')->once()->with($this->asset)->andReturn(m::self()); 137 | $filter->shouldReceive('getFilter')->once()->andReturn('FooFilter'); 138 | 139 | $this->asset->apply(array('FooFilter' => function($filter) 140 | { 141 | $filter->applied = true; 142 | })); 143 | 144 | $this->assertTrue($filter->applied); 145 | } 146 | 147 | 148 | public function testFiltersArePreparedCorrectly() 149 | { 150 | $fooFilter = m::mock('Basset\Filter\Filter', array(m::mock('Illuminate\Log\Writer'), 'FooFilter', array(), 'testing'))->shouldDeferMissing(); 151 | $fooFilterInstance = m::mock('stdClass, Assetic\Filter\FilterInterface'); 152 | $fooFilter->shouldReceive('getClassName')->once()->andReturn($fooFilterInstance); 153 | 154 | $barFilter = m::mock('Basset\Filter\Filter', array($barLog = m::mock('Illuminate\Log\Writer'), 'BarFilter', array(), 'testing'))->shouldDeferMissing(); 155 | $barFilter->shouldReceive('getClassName')->once()->andReturn(m::mock('stdClass, Assetic\Filter\FilterInterface')); 156 | 157 | $bazFilter = m::mock('Basset\Filter\Filter', array($bazLog = m::mock('Illuminate\Log\Writer'), 'BazFilter', array(), 'testing'))->shouldDeferMissing(); 158 | $bazFilter->shouldReceive('getClassName')->once()->andReturn(m::mock('stdClass, Assetic\Filter\FilterInterface')); 159 | 160 | $quxFilter = m::mock('Basset\Filter\Filter', array($quxLog = m::mock('Illuminate\Log\Writer'), 'QuxFilter', array(), 'testing'))->shouldDeferMissing(); 161 | $quxFilter->shouldReceive('getClassName')->once()->andReturn(m::mock('stdClass, Assetic\Filter\FilterInterface')); 162 | 163 | $vanFilter = m::mock('Basset\Filter\Filter', array(m::mock('Illuminate\Log\Writer'), 'VanFilter', array(), 'testing'))->shouldDeferMissing(); 164 | $vanFilterInstance = m::mock('stdClass, Assetic\Filter\FilterInterface'); 165 | $vanFilter->shouldReceive('getClassName')->once()->andReturn($vanFilterInstance); 166 | 167 | $this->asset->apply($fooFilter); 168 | $this->asset->apply($barFilter)->whenAssetIsJavascript(); 169 | $this->asset->apply($bazFilter)->whenEnvironmentIs('production'); 170 | $this->asset->apply($quxFilter)->whenAssetIs('.*\.js'); 171 | $this->asset->apply($vanFilter)->whenAssetIs('.*\.sass'); 172 | 173 | $filters = $this->asset->prepareFilters(); 174 | 175 | $this->assertTrue($filters->has('FooFilter')); 176 | $this->assertTrue($filters->has('VanFilter')); 177 | $this->assertFalse($filters->has('BarFilter')); 178 | $this->assertFalse($filters->has('BazFilter')); 179 | $this->assertFalse($filters->has('QuxFilter')); 180 | } 181 | 182 | 183 | public function testAssetIsBuiltCorrectly() 184 | { 185 | $contents = 'html { background-color: #fff; }'; 186 | 187 | $instantiatedFilter = m::mock('Assetic\Filter\FilterInterface'); 188 | $instantiatedFilter->shouldReceive('filterLoad')->once()->andReturn(null); 189 | $instantiatedFilter->shouldReceive('filterDump')->once()->andReturnUsing(function($asset) use ($contents) 190 | { 191 | $asset->setContent(str_replace('html', 'body', $contents)); 192 | }); 193 | 194 | $filter = m::mock('Basset\Filter\Filter')->shouldDeferMissing(); 195 | $filter->shouldReceive('setResource')->once()->with($this->asset)->andReturn(m::self()); 196 | $filter->shouldReceive('getFilter')->once()->andReturn('BodyFilter'); 197 | $filter->shouldReceive('getInstance')->once()->andReturn($instantiatedFilter); 198 | 199 | 200 | $config = m::mock('Illuminate\Config\Repository'); 201 | 202 | $this->files->shouldReceive('getRemote')->once()->with('path/to/public/foo/bar.sass')->andReturn($contents); 203 | 204 | $this->asset->apply($filter); 205 | 206 | $this->assertEquals('body { background-color: #fff; }', $this->asset->build()); 207 | } 208 | 209 | 210 | } -------------------------------------------------------------------------------- /tests/Basset/ServerTest.php: -------------------------------------------------------------------------------- 1 | app = array( 23 | 'env' => 'testing', 24 | 'url' => new UrlGenerator(new RouteCollection, Request::create('http://localhost', 'GET')), 25 | 'config' => m::mock('Illuminate\Config\Repository'), 26 | 'basset' => m::mock('Basset\Environment'), 27 | 'basset.manifest' => new Manifest(new Filesystem, 'meta'), 28 | 'basset.builder' => m::mock('Basset\Builder\Builder'), 29 | 'basset.builder.cleaner' => m::mock('Basset\Builder\FilesystemCleaner') 30 | ); 31 | 32 | $this->server = new Server($this->app); 33 | } 34 | 35 | 36 | public function testServingInvalidCollectionReturnsHtmlComment() 37 | { 38 | $this->app['basset']->shouldReceive('offsetExists')->once()->with('foo')->andReturn(false); 39 | $this->assertEquals('', $this->server->serve('foo', 'stylesheets')); 40 | } 41 | 42 | 43 | /** 44 | * @dataProvider providerServingProductionCollectionReturnsExpectedHtml 45 | */ 46 | public function testServingProductionCollectionReturnsExpectedHtml($name, $group, $fingerprint, $expected) 47 | { 48 | $this->app['basset']->shouldReceive(array('offsetExists' => true, 'offsetGet' => $collection = m::mock('Basset\Collection')))->with($name); 49 | 50 | $this->app['config']->shouldReceive('get')->once()->with('basset::production')->andReturn('testing'); 51 | 52 | $collection->shouldReceive('getAssetsOnlyRaw')->with($group)->andReturn(array()); 53 | $collection->shouldReceive('getIdentifier')->andReturn($name); 54 | 55 | $entry = $this->app['basset.manifest']->make($collection); 56 | $entry->setProductionFingerprint($group, $fingerprint); 57 | 58 | $this->app['config']->shouldReceive('get')->with('basset::build_path')->andReturn('assets'); 59 | 60 | $this->assertEquals($expected, $this->server->{$group}($name)); 61 | } 62 | 63 | 64 | public function providerServingProductionCollectionReturnsExpectedHtml() 65 | { 66 | return array( 67 | array('foo', 'stylesheets', 'bar-123.css', ''), 68 | array('bar', 'javascripts', 'baz-321.js', ''), 69 | ); 70 | } 71 | 72 | 73 | public function testServingDevelopmentCollectionReturnsExpectedHtml() 74 | { 75 | $this->app['basset']->shouldReceive(array('offsetExists' => true, 'offsetGet' => $collection = m::mock('Basset\Collection')))->with('foo'); 76 | 77 | $this->app['config']->shouldReceive('get')->once()->with('basset::production')->andReturn('prod'); 78 | 79 | $this->app['basset.builder']->shouldReceive('buildAsDevelopment')->once()->with($collection, 'stylesheets'); 80 | $this->app['basset.builder.cleaner']->shouldReceive('clean')->once()->with('foo'); 81 | 82 | $collection->shouldReceive('getIdentifier')->andReturn('foo'); 83 | $collection->shouldReceive('getAssetsWithRaw')->once()->with('stylesheets')->andReturn($assets = array( 84 | m::mock('Basset\Asset'), 85 | m::mock('Basset\Asset'), 86 | m::mock('Basset\Asset') 87 | )); 88 | 89 | $assets[0]->shouldReceive('isRaw')->once()->andReturn(false); 90 | $assets[0]->shouldReceive('getGroup')->once()->andReturn('stylesheets'); 91 | $assets[0]->shouldReceive('getRelativePath')->once()->andReturn('bar.less'); 92 | $assets[1]->shouldReceive('isRaw')->once()->andReturn(true); 93 | $assets[1]->shouldReceive('getRelativePath')->once()->andReturn('qux.css'); 94 | $assets[2]->shouldReceive('isRaw')->once()->andReturn(false); 95 | $assets[2]->shouldReceive('getGroup')->once()->andReturn('stylesheets'); 96 | $assets[2]->shouldReceive('getRelativePath')->once()->andReturn('baz.sass'); 97 | 98 | $entry = $this->app['basset.manifest']->make($collection); 99 | $entry->addDevelopmentAsset('bar.less', 'bar.css', 'stylesheets'); 100 | $entry->addDevelopmentAsset('baz.sass', 'baz.css', 'stylesheets'); 101 | 102 | $this->app['config']->shouldReceive('get')->with('basset::build_path')->andReturn('assets'); 103 | 104 | $expected = ''.PHP_EOL. 105 | ''.PHP_EOL. 106 | ''; 107 | $this->assertEquals($expected, $this->server->serve('foo', 'stylesheets')); 108 | } 109 | 110 | 111 | public function testRawAssetsAreServedBeforeBuiltCollectionHtml() 112 | { 113 | $this->app['basset']->shouldReceive(array('offsetExists' => true, 'offsetGet' => $collection = m::mock('Basset\Collection')))->with('foo'); 114 | 115 | $this->app['config']->shouldReceive('get')->once()->with('basset::production')->andReturn('testing'); 116 | 117 | $collection->shouldReceive('getAssetsOnlyRaw')->with('stylesheets')->andReturn(array($asset = m::mock('Basset\Asset'))); 118 | $collection->shouldReceive('getIdentifier')->andReturn('foo'); 119 | 120 | $asset->shouldReceive('getRelativePath')->andReturn('css/baz.css'); 121 | 122 | $entry = $this->app['basset.manifest']->make($collection); 123 | $entry->setProductionFingerprint('stylesheets', 'bar-123.css'); 124 | 125 | $this->app['config']->shouldReceive('get')->with('basset::build_path')->andReturn('assets'); 126 | 127 | $expected = ''.PHP_EOL. 128 | ''; 129 | $this->assertEquals($expected, $this->server->collection('foo.css')); 130 | } 131 | 132 | 133 | public function testServingCollectionsWithCustomFormat() 134 | { 135 | $this->app['basset']->shouldReceive(array('offsetExists' => true, 'offsetGet' => $collection = m::mock('Basset\Collection')))->with('foo'); 136 | 137 | $this->app['config']->shouldReceive('get')->once()->with('basset::production')->andReturn('testing'); 138 | 139 | $collection->shouldReceive('getAssetsOnlyRaw')->with('stylesheets')->andReturn(array()); 140 | $collection->shouldReceive('getIdentifier')->andReturn('foo'); 141 | 142 | $entry = $this->app['basset.manifest']->make($collection); 143 | $entry->setProductionFingerprint('stylesheets', 'foo-123.css'); 144 | 145 | $this->app['config']->shouldReceive('get')->with('basset::build_path')->andReturn('assets'); 146 | 147 | $expected = ''; 148 | $this->assertEquals($expected, $this->server->stylesheets('foo', '')); 149 | } 150 | 151 | 152 | public function testServingRawAssetsOnGivenEnvironment() 153 | { 154 | $this->app['basset']->shouldReceive(array('offsetExists' => true, 'offsetGet' => $collection = m::mock('Basset\Collection')))->with('foo'); 155 | $this->app['config']->shouldReceive('get')->once()->with('basset::production')->andReturn('production'); 156 | 157 | $this->app['basset.builder']->shouldReceive('buildAsDevelopment')->once()->with($collection, 'stylesheets'); 158 | $this->app['basset.builder.cleaner']->shouldReceive('clean')->once()->with('foo'); 159 | 160 | $collection->shouldReceive('getIdentifier')->andReturn('foo'); 161 | $collection->shouldReceive('getAssetsWithRaw')->once()->with('stylesheets')->andReturn($assets = array( 162 | m::mock('Basset\Asset', array(new Filesystem, m::mock('Basset\Factory\FactoryManager'), 'testing', null, null))->shouldDeferMissing(), 163 | m::mock('Basset\Asset')->shouldDeferMissing() 164 | )); 165 | 166 | $assets[0]->rawOnEnvironment('testing'); 167 | $assets[0]->shouldReceive('getRelativePath')->once()->andReturn('bar.css'); 168 | $assets[1]->shouldReceive('isRaw')->once()->andReturn(false); 169 | $assets[1]->shouldReceive('getGroup')->once()->andReturn('stylesheets'); 170 | $assets[1]->shouldReceive('getRelativePath')->once()->andReturn('baz.sass'); 171 | 172 | $entry = $this->app['basset.manifest']->make($collection); 173 | $entry->addDevelopmentAsset('baz.sass', 'baz.css', 'stylesheets'); 174 | 175 | $this->app['config']->shouldReceive('get')->with('basset::build_path')->andReturn('assets'); 176 | 177 | $expected = ''.PHP_EOL. 178 | ''; 179 | $this->assertEquals($expected, $this->server->serve('foo', 'stylesheets')); 180 | } 181 | 182 | 183 | } -------------------------------------------------------------------------------- /tests/Basset/Filter/FilterTest.php: -------------------------------------------------------------------------------- 1 | log = m::mock('Illuminate\Log\Writer'); 17 | $this->filter = m::mock('Basset\Filter\Filter', array($this->log, 'FooFilter', array(), 'testing'))->shouldDeferMissing(); 18 | $this->filter->setResource($this->resource = m::mock('Basset\Filter\Filterable')); 19 | } 20 | 21 | 22 | public function testSettingOfFilterInstantiationArguments() 23 | { 24 | $this->filter->setArguments('bar', 'baz'); 25 | 26 | $arguments = $this->filter->getArguments(); 27 | 28 | $this->assertEquals(array('bar', 'baz'), $arguments); 29 | } 30 | 31 | 32 | public function testSettingOfFilterInstantiationArgumentsOverwritesExistingArguments() 33 | { 34 | $this->filter->setArguments('foo', 'bar'); 35 | $this->filter->setArguments('baz'); 36 | 37 | $arguments = $this->filter->getArguments(); 38 | 39 | $this->assertEquals(array('baz'), $arguments); 40 | } 41 | 42 | 43 | public function testSettingFilterEnvironmentRequirement() 44 | { 45 | $this->filter->whenEnvironmentIs('testing'); 46 | $this->assertTrue($this->filter->processRequirements()); 47 | } 48 | 49 | 50 | public function testSettingFilterStylesheetGroupRestrictionRequirement() 51 | { 52 | $this->resource->shouldReceive('isStylesheet')->once()->andReturn(false); 53 | $this->filter->whenAssetIsStylesheet(); 54 | $this->assertFalse($this->filter->processRequirements()); 55 | } 56 | 57 | 58 | public function testSettingFilterJavascriptGroupRestrictionRequirement() 59 | { 60 | $this->resource->shouldReceive('isJavascript')->once()->andReturn(true); 61 | $this->filter->whenAssetIsJavascript(); 62 | $this->assertTrue($this->filter->processRequirements()); 63 | } 64 | 65 | 66 | public function testSettingAssetNameIsRequirement() 67 | { 68 | $this->resource->shouldReceive('getRelativePath')->times(3)->andReturn('foo/bar.css'); 69 | 70 | $this->filter->whenAssetIs('.*\.css'); 71 | $this->assertTrue($this->filter->processRequirements()); 72 | 73 | $this->filter->whenAssetIs('foo/baz.css'); 74 | $this->assertFalse($this->filter->processRequirements()); 75 | } 76 | 77 | 78 | public function testSettingClassExistsFilterRequirement() 79 | { 80 | $this->filter->whenClassExists('FilterTest'); 81 | $this->assertTrue($this->filter->processRequirements()); 82 | 83 | $this->filter->whenClassExists('FooBarBaz'); 84 | $this->assertFalse($this->filter->processRequirements()); 85 | } 86 | 87 | 88 | public function testSettingProductionBuildFilterRequirement() 89 | { 90 | $this->filter->whenProductionBuild(); 91 | $this->assertFalse($this->filter->processRequirements()); 92 | 93 | $this->filter->setProduction(true); 94 | $this->assertTrue($this->filter->processRequirements()); 95 | } 96 | 97 | 98 | public function testSettingDevelopmentBuildFilterRequirement() 99 | { 100 | $this->filter->whenDevelopmentBuild(); 101 | $this->assertTrue($this->filter->processRequirements()); 102 | 103 | $this->filter->setProduction(true); 104 | $this->assertFalse($this->filter->processRequirements()); 105 | } 106 | 107 | 108 | public function testSettingCustomFilterRequirement() 109 | { 110 | $this->resource->shouldReceive('fooBar')->times(3)->andReturn(true, true, false); 111 | 112 | $this->filter->when(function($asset) 113 | { 114 | return $asset->fooBar(); 115 | }); 116 | $this->assertTrue($this->filter->processRequirements()); 117 | 118 | $this->filter->when(function($asset) 119 | { 120 | return $asset->fooBar(); 121 | }); 122 | $this->assertFalse($this->filter->processRequirements()); 123 | } 124 | 125 | 126 | public function testInstantiationOfFiltersWithNoArguments() 127 | { 128 | $this->filter->shouldReceive('getClassName')->once()->andReturn('FilterStub'); 129 | $instance = $this->filter->getInstance(); 130 | $this->assertInstanceOf('FilterStub', $instance); 131 | } 132 | 133 | 134 | public function testInstantiationOfFiltersWithArguments() 135 | { 136 | $this->filter->shouldReceive('getClassName')->once()->andReturn('FilterWithConstructorStub'); 137 | $this->filter->setArguments('bar'); 138 | $instance = $this->filter->getInstance(); 139 | $this->assertEquals('bar', $instance->getFooBin()); 140 | } 141 | 142 | 143 | public function testInstantiationOfFiltersWithBeforeFilteringCallback() 144 | { 145 | $this->filter->shouldReceive('getClassName')->once()->andReturn('FilterStub'); 146 | $this->filter->beforeFiltering(function($filter) 147 | { 148 | $filter->setFooBin('bar'); 149 | }); 150 | $instance = $this->filter->getInstance(); 151 | $this->assertEquals('bar', $instance->getFooBin()); 152 | } 153 | 154 | 155 | public function testInvalidMethodsAreHandledByResource() 156 | { 157 | $filter = new Basset\Filter\Filter($this->log, 'FooFilter', array(), 'testing'); 158 | $filter->setResource($this->resource); 159 | $this->resource->shouldReceive('foo')->once()->andReturn('bar'); 160 | $this->assertEquals('bar', $filter->foo()); 161 | } 162 | 163 | 164 | public function testFindingOfMissingConstructorArgsWithInvalidClassReturnsCurrentInstance() 165 | { 166 | $this->filter->shouldReceive('getClassName')->once()->andReturn(null); 167 | $this->assertEquals($this->filter, $this->filter->findMissingConstructorArgs()); 168 | } 169 | 170 | 171 | public function testFindingOfMissingConstructorArgsSkipsExistingArgument() 172 | { 173 | $this->filter->shouldReceive('getClassName')->once()->andReturn('FilterWithConstructorStub'); 174 | $this->filter->shouldReceive('getExecutableFinder')->once()->andReturn(m::mock('Symfony\Component\Process\ExecutableFinder')); 175 | $this->filter->setArguments('foo'); 176 | $this->filter->findMissingConstructorArgs(); 177 | $this->assertContains('foo', $this->filter->getArguments()); 178 | } 179 | 180 | 181 | public function testFindingOfMissingConstructorArgsViaEnvironmentVariable() 182 | { 183 | $this->filter->shouldReceive('getClassName')->once()->andReturn('FilterWithConstructorStub'); 184 | $this->filter->shouldReceive('getExecutableFinder')->once()->andReturn(m::mock('Symfony\Component\Process\ExecutableFinder')); 185 | $this->filter->shouldReceive('getEnvironmentVariable')->once()->with('foo_bin')->andReturn('path/to/foo/bin'); 186 | $this->filter->findMissingConstructorArgs(); 187 | $this->assertContains('path/to/foo/bin', $this->filter->getArguments()); 188 | } 189 | 190 | 191 | public function testFindingOfMissingConstructorArgsViaExecutableFinder() 192 | { 193 | $this->filter->shouldReceive('getClassName')->once()->andReturn('FilterWithConstructorStub'); 194 | $this->filter->shouldReceive('getExecutableFinder')->once()->andReturn($finder = m::mock('Symfony\Component\Process\ExecutableFinder')); 195 | $finder->shouldReceive('find')->once()->with('foo')->andReturn('path/to/foo/bin'); 196 | $this->filter->findMissingConstructorArgs(); 197 | $this->assertContains('path/to/foo/bin', $this->filter->getArguments()); 198 | } 199 | 200 | 201 | public function testFindingOfMissingConstructorArgsSetsFilterNodePaths() 202 | { 203 | $filter = m::mock('Basset\Filter\Filter', array($this->log, 'FooFilter', array('path/to/node'), 'testing'))->shouldDeferMissing(); 204 | $filter->setResource($this->resource); 205 | $filter->shouldReceive('getClassName')->once()->andReturn('FilterWithConstructorStub'); 206 | $filter->shouldReceive('getExecutableFinder')->once()->andReturn($finder = m::mock('Symfony\Component\Process\ExecutableFinder')); 207 | $filter->shouldReceive('getEnvironmentVariable')->once()->with('foo_bin')->andReturn('path/to/foo/bin'); 208 | $filter->findMissingConstructorArgs(); 209 | $this->assertContains(array('path/to/node'), $filter->getArguments()); 210 | } 211 | 212 | 213 | public function testFindingOfMissingConstructorArgsIgnoresFilterWithInvalidExecutables() 214 | { 215 | $this->log->shouldReceive('error')->once(); 216 | $this->filter->shouldReceive('getClassName')->once()->andReturn('FilterWithConstructorStub'); 217 | $this->filter->shouldReceive('getExecutableFinder')->once()->andReturn($finder = m::mock('Symfony\Component\Process\ExecutableFinder')); 218 | $finder->shouldReceive('find')->once()->with('foo')->andReturn(false); 219 | $this->filter->findMissingConstructorArgs(); 220 | $this->assertTrue($this->filter->isIgnored()); 221 | } 222 | 223 | 224 | public function testFindingOfMissingConstructorArgsIsSkippedWhenNoConstructorPresent() 225 | { 226 | $this->filter->shouldReceive('getClassName')->once()->andReturn('FilterStub'); 227 | $this->filter->findMissingConstructorArgs(); 228 | $this->assertEmpty($this->filter->getArguments()); 229 | } 230 | 231 | 232 | } 233 | 234 | 235 | class FilterStub { 236 | 237 | protected $fooBin; 238 | 239 | public function setFooBin($fooBin) 240 | { 241 | $this->fooBin = $fooBin; 242 | } 243 | 244 | public function getFooBin() 245 | { 246 | return $this->fooBin; 247 | } 248 | 249 | } 250 | 251 | 252 | class FilterWithConstructorStub { 253 | 254 | protected $fooBin; 255 | 256 | public function __construct($fooBin, $nodePaths = array()) 257 | { 258 | $this->fooBin = $fooBin; 259 | } 260 | 261 | public function getFooBin() 262 | { 263 | return $this->fooBin; 264 | } 265 | 266 | } -------------------------------------------------------------------------------- /src/config/config.php: -------------------------------------------------------------------------------- 1 | array( 30 | 31 | 'application' => function($collection) 32 | { 33 | // Switch to the stylesheets directory and require the "less" and "sass" directories. 34 | // These directories both have a filter applied to them so that the built 35 | // collection will contain valid CSS. 36 | $directory = $collection->directory('assets/stylesheets', function($collection) 37 | { 38 | $collection->requireDirectory('less')->apply('Less'); 39 | $collection->requireDirectory('sass')->apply('Sass'); 40 | $collection->requireDirectory(); 41 | }); 42 | 43 | $directory->apply('CssMin'); 44 | $directory->apply('UriRewriteFilter'); 45 | 46 | // Switch to the javascripts directory and require the "coffeescript" directory. As 47 | // with the above directories we'll apply the CoffeeScript filter to the directory 48 | // so the built collection contains valid JS. 49 | $directory = $collection->directory('assets/javascripts', function($collection) 50 | { 51 | $collection->requireDirectory('coffeescripts')->apply('CoffeeScript'); 52 | $collection->requireDirectory(); 53 | }); 54 | 55 | $directory->apply('JsMin'); 56 | } 57 | 58 | ), 59 | 60 | /* 61 | |-------------------------------------------------------------------------- 62 | | Production Environment 63 | |-------------------------------------------------------------------------- 64 | | 65 | | Basset needs to know what your production environment is so that it can 66 | | respond with the correct assets. When in production Basset will attempt 67 | | to return any built collections. If a collection has not been built 68 | | Basset will dynamically route to each asset in the collection and apply 69 | | the filters. 70 | | 71 | | The last method can be very taxing so it's highly recommended that 72 | | collections are built when deploying to a production environment. 73 | | 74 | | You can supply an array of production environment names if you need to. 75 | | 76 | */ 77 | 78 | 'production' => array('production', 'prod'), 79 | 80 | /* 81 | |-------------------------------------------------------------------------- 82 | | Build Path 83 | |-------------------------------------------------------------------------- 84 | | 85 | | When assets are built with Artisan they will be stored within a directory 86 | | relative to the public directory. 87 | | 88 | | If the directory does not exist Basset will attempt to create it. 89 | | 90 | */ 91 | 92 | 'build_path' => 'builds', 93 | 94 | /* 95 | |-------------------------------------------------------------------------- 96 | | Debug 97 | |-------------------------------------------------------------------------- 98 | | 99 | | Enable debugging to have potential errors or problems encountered 100 | | during operation logged to a rotating file setup. 101 | | 102 | */ 103 | 104 | 'debug' => false, 105 | 106 | /* 107 | |-------------------------------------------------------------------------- 108 | | Node Paths 109 | |-------------------------------------------------------------------------- 110 | | 111 | | Many filters use Node to build assets. We recommend you install your 112 | | Node modules locally at the root of your application, however you can 113 | | specify additional paths to your modules. 114 | | 115 | */ 116 | 117 | 'node_paths' => array( 118 | 119 | base_path().'/node_modules' 120 | 121 | ), 122 | 123 | /* 124 | |-------------------------------------------------------------------------- 125 | | Gzip Built Collections 126 | |-------------------------------------------------------------------------- 127 | | 128 | | To get the most speed and compression out of Basset you can enable Gzip 129 | | for every collection that is built via the command line. This is applied 130 | | to both collection builds and development builds. 131 | | 132 | | You can use the --gzip switch for on-the-fly Gzipping of collections. 133 | | 134 | */ 135 | 136 | 'gzip' => false, 137 | 138 | /* 139 | |-------------------------------------------------------------------------- 140 | | Asset and Filter Aliases 141 | |-------------------------------------------------------------------------- 142 | | 143 | | You can define aliases for commonly used assets or filters. 144 | | An example of an asset alias: 145 | | 146 | | 'layout' => 'stylesheets/layout/master.css' 147 | | 148 | | Filter aliases are slightly different. You can define a simple alias 149 | | similar to an asset alias. 150 | | 151 | | 'YuiCss' => 'Yui\CssCompressorFilter' 152 | | 153 | | However if you want to pass in options to an aliased filter then define 154 | | the alias as a nested array. The key should be the filter and the value 155 | | should be a callback closure where you can set parameters for a filters 156 | | constructor, etc. 157 | | 158 | | 'YuiCss' => array('Yui\CssCompressorFilter', function($filter) 159 | | { 160 | | $filter->setArguments('path/to/jar'); 161 | | }) 162 | | 163 | | 164 | */ 165 | 166 | 'aliases' => array( 167 | 168 | 'assets' => array(), 169 | 170 | 'filters' => array( 171 | 172 | /* 173 | |-------------------------------------------------------------------------- 174 | | Less Filter Alias 175 | |-------------------------------------------------------------------------- 176 | | 177 | | Filter is applied only when asset has a ".less" extension and it will 178 | | attempt to find missing constructor arguments. 179 | | 180 | */ 181 | 182 | 'Less' => array('LessFilter', function($filter) 183 | { 184 | $filter->whenAssetIs('.*\.less')->findMissingConstructorArgs(); 185 | }), 186 | 187 | /* 188 | |-------------------------------------------------------------------------- 189 | | Sass Filter Alias 190 | |-------------------------------------------------------------------------- 191 | | 192 | | Filter is applied only when asset has a ".sass" or ".scss" extension and 193 | | it will attempt to find missing constructor arguments. 194 | | 195 | */ 196 | 197 | 'Sass' => array('Sass\ScssFilter', function($filter) 198 | { 199 | $filter->whenAssetIs('.*\.(sass|scss)')->findMissingConstructorArgs(); 200 | }), 201 | 202 | /* 203 | |-------------------------------------------------------------------------- 204 | | CoffeeScript Filter Alias 205 | |-------------------------------------------------------------------------- 206 | | 207 | | Filter is applied only when asset has a ".coffee" extension and it will 208 | | attempt to find missing constructor arguments. 209 | | 210 | */ 211 | 212 | 'CoffeeScript' => array('CoffeeScriptFilter', function($filter) 213 | { 214 | $filter->whenAssetIs('.*\.coffee')->findMissingConstructorArgs(); 215 | }), 216 | 217 | /* 218 | |-------------------------------------------------------------------------- 219 | | CssMin Filter Alias 220 | |-------------------------------------------------------------------------- 221 | | 222 | | Filter is applied only when within the production environment and when 223 | | the "CssMin" class exists. 224 | | 225 | */ 226 | 227 | 'CssMin' => array('CssMinFilter', function($filter) 228 | { 229 | $filter->whenAssetIsStylesheet()->whenProductionBuild()->whenClassExists('CssMin'); 230 | }), 231 | 232 | /* 233 | |-------------------------------------------------------------------------- 234 | | JsMin Filter Alias 235 | |-------------------------------------------------------------------------- 236 | | 237 | | Filter is applied only when within the production environment and when 238 | | the "JsMin" class exists. 239 | | 240 | */ 241 | 242 | 'JsMin' => array('JSMinFilter', function($filter) 243 | { 244 | $filter->whenAssetIsJavascript()->whenProductionBuild()->whenClassExists('JSMin'); 245 | }), 246 | 247 | /* 248 | |-------------------------------------------------------------------------- 249 | | UriRewrite Filter Alias 250 | |-------------------------------------------------------------------------- 251 | | 252 | | Filter gets a default argument of the path to the public directory. 253 | | 254 | */ 255 | 256 | 'UriRewriteFilter' => array('UriRewriteFilter', function($filter) 257 | { 258 | $filter->setArguments(public_path())->whenAssetIsStylesheet(); 259 | }) 260 | 261 | ) 262 | 263 | ) 264 | 265 | ); 266 | -------------------------------------------------------------------------------- /src/Basset/Asset.php: -------------------------------------------------------------------------------- 1 | array('css', 'sass', 'scss', 'less', 'styl', 'roo', 'gss'), 82 | 'javascripts' => array('js', 'coffee', 'dart', 'ts', 'hbs') 83 | ); 84 | 85 | /** 86 | * Create a new asset instance. 87 | * 88 | * @param \Illuminate\Filesystem\Filesystem $files 89 | * @param \Basset\Factory\FactoryManager $factory 90 | * @param string $appEnvironment 91 | * @param string $absolutePath 92 | * @param string $relativePath 93 | * @return void 94 | */ 95 | public function __construct(Filesystem $files, FactoryManager $factory, $appEnvironment, $absolutePath, $relativePath) 96 | { 97 | parent::__construct(); 98 | 99 | $this->files = $files; 100 | $this->factory = $factory; 101 | $this->appEnvironment = $appEnvironment; 102 | $this->absolutePath = $absolutePath; 103 | $this->relativePath = $relativePath; 104 | } 105 | 106 | /** 107 | * Get the absolute path to the asset. 108 | * 109 | * @return string 110 | */ 111 | public function getAbsolutePath() 112 | { 113 | return $this->absolutePath; 114 | } 115 | 116 | /** 117 | * Get the relative path to the asset. 118 | * 119 | * @return string 120 | */ 121 | public function getRelativePath() 122 | { 123 | return $this->relativePath; 124 | } 125 | 126 | /** 127 | * Get the build path to the asset. 128 | * 129 | * @return string 130 | */ 131 | public function getBuildPath() 132 | { 133 | $path = pathinfo($this->relativePath); 134 | 135 | $fingerprint = md5($this->filters->map(function($f) { return $f->getFilter(); })->toJson().$this->getLastModified()); 136 | 137 | return "{$path['dirname']}/{$path['filename']}-{$fingerprint}.{$this->getBuildExtension()}"; 138 | } 139 | 140 | /** 141 | * Get the build extension of the asset. 142 | * 143 | * @return string 144 | */ 145 | public function getBuildExtension() 146 | { 147 | return $this->isJavascript() ? 'js' : 'css'; 148 | } 149 | 150 | /** 151 | * Get the last modified time of the asset. 152 | * 153 | * @return int 154 | */ 155 | public function getLastModified() 156 | { 157 | if ($this->lastModified) 158 | { 159 | return $this->lastModified; 160 | } 161 | 162 | return $this->lastModified = $this->isRemote() ? null : $this->files->lastModified($this->absolutePath); 163 | } 164 | 165 | /** 166 | * Determine if asset is a javascript. 167 | * 168 | * @return bool 169 | */ 170 | public function isJavascript() 171 | { 172 | return $this->getGroup() == 'javascripts'; 173 | } 174 | 175 | /** 176 | * Determine if asset is a stylesheet. 177 | * 178 | * @return bool 179 | */ 180 | public function isStylesheet() 181 | { 182 | return $this->getGroup() == 'stylesheets'; 183 | } 184 | 185 | /** 186 | * Determine if asset is remotely hosted. 187 | * 188 | * @return bool 189 | */ 190 | public function isRemote() 191 | { 192 | return starts_with($this->absolutePath, '//') or (bool) filter_var($this->absolutePath, FILTER_VALIDATE_URL); 193 | } 194 | 195 | /** 196 | * Alias for \Basset\Asset::setOrder(1) 197 | * 198 | * @return Basset\Asset 199 | */ 200 | public function first() 201 | { 202 | return $this->setOrder(1); 203 | } 204 | 205 | /** 206 | * Alias for \Basset\Asset::setOrder(2) 207 | * 208 | * @return \Basset\Asset 209 | */ 210 | public function second() 211 | { 212 | return $this->setOrder(2); 213 | } 214 | 215 | /** 216 | * Alias for \Basset\Asset::setOrder(3) 217 | * 218 | * @return \Basset\Asset 219 | */ 220 | public function third() 221 | { 222 | return $this->setOrder(3); 223 | } 224 | 225 | /** 226 | * Alias for \Basset\Asset::setOrder() 227 | * 228 | * @param int $order 229 | * @return \Basset\Asset 230 | */ 231 | public function order($order) 232 | { 233 | return $this->setOrder($order); 234 | } 235 | 236 | /** 237 | * Set the order of the outputted asset. 238 | * 239 | * @param int $order 240 | * @return \Basset\Asset 241 | */ 242 | public function setOrder($order) 243 | { 244 | $this->order = $order; 245 | 246 | return $this; 247 | } 248 | 249 | /** 250 | * Get the assets order. 251 | * 252 | * @return int|null 253 | */ 254 | public function getOrder() 255 | { 256 | return $this->order; 257 | } 258 | 259 | /** 260 | * Set the assets group. 261 | * 262 | * @param string $group 263 | * @return \Basset\Asset 264 | */ 265 | public function setGroup($group) 266 | { 267 | $this->group = $group; 268 | 269 | return $this; 270 | } 271 | 272 | /** 273 | * Get the assets group. 274 | * 275 | * @return string 276 | */ 277 | public function getGroup() 278 | { 279 | if ($this->group) 280 | { 281 | return $this->group; 282 | } 283 | 284 | return $this->group = $this->detectGroupFromExtension() ?: $this->detectGroupFromContentType(); 285 | } 286 | 287 | /** 288 | * Detect the group from the content type using cURL. 289 | * 290 | * @return null|string 291 | */ 292 | protected function detectGroupFromContentType() 293 | { 294 | if (extension_loaded('curl')) 295 | { 296 | $this->getLogger()->warning('Attempting to determine asset group using cURL. This may have a considerable effect on application speed.'); 297 | 298 | $handler = curl_init($this->absolutePath); 299 | 300 | curl_setopt($handler, CURLOPT_RETURNTRANSFER, true); 301 | curl_setopt($handler, CURLOPT_FOLLOWLOCATION, true); 302 | curl_setopt($handler, CURLOPT_HEADER, true); 303 | curl_setopt($handler, CURLOPT_NOBODY, true); 304 | curl_setopt($handler, CURLOPT_SSL_VERIFYPEER, false); 305 | 306 | curl_exec($handler); 307 | 308 | if ( ! curl_errno($handler)) 309 | { 310 | $contentType = curl_getinfo($handler, CURLINFO_CONTENT_TYPE); 311 | 312 | return starts_with($contentType, 'text/css') ? 'stylesheets' : 'javascripts'; 313 | } 314 | } 315 | } 316 | 317 | /** 318 | * Detect group from the assets extension. 319 | * 320 | * @return string 321 | */ 322 | protected function detectGroupFromExtension() 323 | { 324 | $extension = pathinfo($this->absolutePath, PATHINFO_EXTENSION); 325 | 326 | foreach (array('stylesheets', 'javascripts') as $group) 327 | { 328 | if (in_array($extension, $this->allowedExtensions[$group])) 329 | { 330 | return $group; 331 | } 332 | } 333 | } 334 | 335 | /** 336 | * A raw asset is just excluded from the build process. 337 | * 338 | * @return \Basset\Asset 339 | */ 340 | public function raw() 341 | { 342 | $this->raw = true; 343 | 344 | return $this; 345 | } 346 | 347 | /** 348 | * Sets the asset to be served raw when the application is running in a given environment. 349 | * 350 | * @param string|array $environment 351 | * @return \Basset\Asset 352 | */ 353 | public function rawOnEnvironment() 354 | { 355 | $environments = array_flatten(func_get_args()); 356 | 357 | if (in_array($this->appEnvironment, $environments)) 358 | { 359 | return $this->raw(); 360 | } 361 | 362 | return $this; 363 | } 364 | 365 | /** 366 | * Determines if the asset is to be served raw. 367 | * 368 | * @return bool 369 | */ 370 | public function isRaw() 371 | { 372 | return $this->raw; 373 | } 374 | 375 | /** 376 | * Get the asset contents. 377 | * 378 | * @return string 379 | */ 380 | public function getContent() 381 | { 382 | return $this->files->getRemote($this->absolutePath); 383 | } 384 | 385 | /** 386 | * Build the asset. 387 | * 388 | * @param bool $production 389 | * @return string 390 | */ 391 | public function build($production = false) 392 | { 393 | $filters = $this->prepareFilters($production); 394 | 395 | $asset = new StringAsset($this->getContent(), $filters->all(), dirname($this->absolutePath), basename($this->absolutePath)); 396 | 397 | return $asset->dump(); 398 | } 399 | 400 | /** 401 | * Prepare the filters applied to the asset. 402 | * 403 | * @param bool $production 404 | * @return \Illuminate\Support\Collection 405 | */ 406 | public function prepareFilters($production = false) 407 | { 408 | $filters = $this->filters->map(function($filter) use ($production) 409 | { 410 | $filter->setProduction($production); 411 | 412 | return $filter->getInstance(); 413 | }); 414 | 415 | return $filters->filter(function($filter) { return $filter instanceof FilterInterface; }); 416 | } 417 | 418 | } -------------------------------------------------------------------------------- /tests/Basset/DirectoryTest.php: -------------------------------------------------------------------------------- 1 | files = m::mock('Illuminate\Filesystem\Filesystem'); 20 | $this->finder = m::mock('Basset\AssetFinder'); 21 | $this->factory = m::mock('Basset\Factory\FactoryManager'); 22 | $this->asset = m::mock('Basset\Factory\AssetFactory', array($this->files, $this->factory, 'testing', 'path/to/public'))->shouldDeferMissing(); 23 | 24 | $this->factory->shouldReceive('get')->with('asset')->andReturn($this->asset); 25 | 26 | $this->directory = new Directory($this->factory, $this->finder, 'foo'); 27 | } 28 | 29 | 30 | public function getAssetInstance($absolute = null, $relative = null) 31 | { 32 | return new Asset($this->files, $this->factory, 'testing', $absolute, $relative); 33 | } 34 | 35 | 36 | public function getLoggerMock() 37 | { 38 | return m::mock('Illuminate\Log\Writer'); 39 | } 40 | 41 | 42 | public function testAddingBasicAssetFromPublicDirectory() 43 | { 44 | $asset = $this->getAssetInstance(); 45 | 46 | $this->finder->shouldReceive('find')->once()->with('foo.css')->andReturn('path/to/foo.css'); 47 | $this->asset->shouldReceive('make')->once()->with('path/to/foo.css')->andReturn($asset); 48 | 49 | $this->assertInstanceOf('Basset\Asset', $this->directory->stylesheet('foo.css')); 50 | $this->assertCount(1, $this->directory->getDirectoryAssets()); 51 | } 52 | 53 | 54 | public function testAddingInvalidAssetReturnsBlankAssetInstance() 55 | { 56 | $asset = $this->getAssetInstance(); 57 | 58 | $this->finder->shouldReceive('find')->once()->with('foo.css')->andThrow('Basset\Exceptions\AssetNotFoundException'); 59 | $this->asset->shouldReceive('make')->once()->with(null)->andReturn($asset); 60 | 61 | $logger = $this->getLoggerMock()->shouldReceive('error')->once()->getMock(); 62 | $this->factory->shouldReceive('getLogger')->once()->andReturn($logger); 63 | 64 | $this->assertInstanceOf('Basset\Asset', $this->directory->stylesheet('foo.css')); 65 | $this->assertCount(0, $this->directory->getDirectoryAssets()); 66 | } 67 | 68 | 69 | public function testAddingAssetFiresCallback() 70 | { 71 | $asset = $this->getAssetInstance(); 72 | 73 | $this->finder->shouldReceive('find')->once()->with('foo.js')->andReturn('path/to/foo.js'); 74 | $this->asset->shouldReceive('make')->once()->with('path/to/foo.js')->andReturn($asset); 75 | 76 | $fired = false; 77 | 78 | $this->directory->javascript('foo.js', function() use (&$fired) { $fired = true; }); 79 | $this->assertTrue($fired); 80 | } 81 | 82 | 83 | public function testChangingWorkingDirectory() 84 | { 85 | $this->finder->shouldReceive('setWorkingDirectory')->once()->with('css')->andReturn('path/to/public/css'); 86 | $this->finder->shouldReceive('resetWorkingDirectory'); 87 | 88 | $this->assertInstanceOf('Basset\Directory', $this->directory->directory('css')); 89 | } 90 | 91 | 92 | public function testChangingWorkingDirectoryToInvalidDirectoryReturnsBlankDirectoryInstance() 93 | { 94 | $this->finder->shouldReceive('setWorkingDirectory')->once()->with('css')->andThrow('Basset\Exceptions\DirectoryNotFoundException'); 95 | $this->factory->shouldReceive('getLogger')->once()->andReturn(m::mock('Illuminate\Log\Writer')->shouldReceive('error')->once()->getMock()); 96 | 97 | $this->assertInstanceOf('Basset\Directory', $this->directory->directory('css')); 98 | } 99 | 100 | 101 | public function testChangingWorkingDirectoryFiresCallback() 102 | { 103 | $this->finder->shouldReceive('setWorkingDirectory')->once()->with('css')->andReturn('path/to/public/css'); 104 | $this->finder->shouldReceive('resetWorkingDirectory'); 105 | 106 | $fired = false; 107 | $this->directory->directory('css', function() use (&$fired) { $fired = true; }); 108 | $this->assertTrue($fired); 109 | } 110 | 111 | 112 | public function testRequireCurrentWorkingDirectory() 113 | { 114 | $directory = m::mock('Basset\Directory[iterateDirectory]', array($this->factory, $this->finder, 'foo')); 115 | $directory->shouldReceive('iterateDirectory')->once()->with('foo')->andReturn($iterator = m::mock('Iterator')); 116 | 117 | $iterator->shouldReceive('rewind')->once(); 118 | $iterator->shouldReceive('valid')->times()->andReturn(true, true, false); 119 | $iterator->shouldReceive('current')->once()->andReturn($files[] = m::mock('SplFileInfo')); 120 | $iterator->shouldReceive('current')->once()->andReturn($files[] = m::mock('SplFileInfo')); 121 | $iterator->shouldReceive('next')->twice(); 122 | 123 | $files[0]->shouldReceive('isFile')->andReturn(true); 124 | $files[0]->shouldReceive('getPathname')->andReturn('foo/bar.css'); 125 | $files[1]->shouldReceive('isFile')->andReturn(false); 126 | 127 | $asset = $this->getAssetInstance(); 128 | $this->finder->shouldReceive('find')->once()->with('foo/bar.css')->andReturn('foo/bar.css'); 129 | $this->asset->shouldReceive('make')->once()->with('foo/bar.css')->andReturn($asset); 130 | 131 | $directory->requireDirectory(); 132 | $this->assertCount(1, $directory->getDirectoryAssets()); 133 | } 134 | 135 | 136 | public function testRequireDirectoryChangesDirectoryAndRequiresNewWorkingDirectory() 137 | { 138 | $directory = m::mock('Basset\Directory[directory]', array($this->factory, $this->finder, 'foo')); 139 | 140 | $requireDirectory = m::mock('Basset\Directory[iterateDirectory]', array($this->factory, $this->finder, 'foo/bar')); 141 | $directory->shouldReceive('directory')->with('bar')->andReturn($requireDirectory); 142 | 143 | $requireDirectory->shouldReceive('iterateDirectory')->once()->with('foo/bar')->andReturn($iterator = m::mock('Iterator')); 144 | 145 | $iterator->shouldReceive('rewind')->once(); 146 | $iterator->shouldReceive('valid')->twice()->andReturn(true, false); 147 | $iterator->shouldReceive('current')->once()->andReturn($file = m::mock('SplFileInfo')); 148 | $iterator->shouldReceive('next')->once(); 149 | 150 | $file->shouldReceive('isFile')->andReturn(true); 151 | $file->shouldReceive('getPathname')->andReturn('foo/bar/baz.css'); 152 | 153 | $asset = $this->getAssetInstance(); 154 | $this->finder->shouldReceive('find')->once()->with('foo/bar/baz.css')->andReturn('foo/bar/baz.css'); 155 | $this->asset->shouldReceive('make')->once()->with('foo/bar/baz.css')->andReturn($asset); 156 | 157 | $directory->requireDirectory('bar'); 158 | $this->assertCount(1, $requireDirectory->getDirectoryAssets()); 159 | } 160 | 161 | 162 | public function testRequireCurrentWorkingDirectoryTree() 163 | { 164 | $directory = m::mock('Basset\Directory[recursivelyIterateDirectory]', array($this->factory, $this->finder, 'foo')); 165 | $directory->shouldReceive('recursivelyIterateDirectory')->once()->with('foo')->andReturn($iterator = m::mock('Iterator')); 166 | 167 | $iterator->shouldReceive('rewind')->once(); 168 | $iterator->shouldReceive('valid')->once()->andReturn(false); 169 | 170 | $directory->requireTree(); 171 | $this->assertCount(0, $directory->getDirectoryAssets()); 172 | } 173 | 174 | 175 | public function testRequireTreeChangesWorkingDirectoryAndRequiresNewDirectoryTree() 176 | { 177 | $directory = m::mock('Basset\Directory[directory]', array($this->factory, $this->finder, 'foo')); 178 | 179 | $requireTree = m::mock('Basset\Directory[recursivelyIterateDirectory]', array($this->factory, $this->finder, 'foo/bar')); 180 | $directory->shouldReceive('directory')->once()->with('bar')->andReturn($requireTree); 181 | 182 | $requireTree->shouldReceive('recursivelyIterateDirectory')->once()->with('foo/bar')->andReturn($iterator = m::mock('Iterator')); 183 | 184 | $iterator->shouldReceive('rewind')->once(); 185 | $iterator->shouldReceive('valid')->once()->andReturn(false); 186 | 187 | $directory->requireTree('bar'); 188 | $this->assertCount(0, $directory->getDirectoryAssets()); 189 | } 190 | 191 | 192 | public function testCanGetFilesystemIterator() 193 | { 194 | $this->assertInstanceOf('FilesystemIterator', $this->directory->iterateDirectory(__DIR__)); 195 | } 196 | 197 | 198 | public function testCanGetRecursiveDirectoryIterator() 199 | { 200 | $this->assertInstanceOf('RecursiveIteratorIterator', $this->directory->recursivelyIterateDirectory(__DIR__)); 201 | } 202 | 203 | 204 | public function testGettingIteratorsReturnsFalseForInvalidDirectories() 205 | { 206 | $this->assertFalse($this->directory->iterateDirectory('foo')); 207 | $this->assertFalse($this->directory->recursivelyIterateDirectory('foo')); 208 | } 209 | 210 | 211 | public function testGettingOfDirectoryPath() 212 | { 213 | $this->assertEquals('foo', $this->directory->getPath()); 214 | } 215 | 216 | 217 | public function testExcludingOfAssetsFromDirectory() 218 | { 219 | $fooAsset = $this->getAssetInstance('path/to/foo.css', 'foo.css'); 220 | $fooAsset->setOrder(1); 221 | $barAsset = $this->getAssetInstance('path/to/bar.css', 'bar.css'); 222 | $barAsset->setOrder(1); 223 | 224 | $this->finder->shouldReceive('find')->once()->with('foo.css')->andReturn('path/to/foo.css'); 225 | $this->asset->shouldReceive('make')->once()->with('path/to/foo.css')->andReturn($fooAsset); 226 | 227 | $this->finder->shouldReceive('find')->once()->with('bar.css')->andReturn('path/to/bar.css'); 228 | $this->asset->shouldReceive('make')->once()->with('path/to/bar.css')->andReturn($barAsset); 229 | 230 | $this->directory->stylesheet('foo.css'); 231 | $this->directory->stylesheet('bar.css'); 232 | 233 | $this->directory->except('foo.css'); 234 | 235 | $this->assertEquals($barAsset, $this->directory->getDirectoryAssets()->first()); 236 | } 237 | 238 | 239 | public function testIncludingOfAssetsFromDirectory() 240 | { 241 | $fooAsset = $this->getAssetInstance('path/to/foo.css', 'foo.css'); 242 | $fooAsset->setOrder(1); 243 | $barAsset = $this->getAssetInstance('path/to/bar.css', 'bar.css'); 244 | $barAsset->setOrder(1); 245 | 246 | $this->finder->shouldReceive('find')->once()->with('foo.css')->andReturn('path/to/foo.css'); 247 | $this->asset->shouldReceive('make')->once()->with('path/to/foo.css')->andReturn($fooAsset); 248 | 249 | $this->finder->shouldReceive('find')->once()->with('bar.css')->andReturn('path/to/bar.css'); 250 | $this->asset->shouldReceive('make')->once()->with('path/to/bar.css')->andReturn($barAsset); 251 | 252 | $this->directory->stylesheet('foo.css'); 253 | $this->directory->stylesheet('bar.css'); 254 | 255 | $this->directory->only('foo.css'); 256 | 257 | $this->assertEquals($fooAsset, $this->directory->getDirectoryAssets()->first()); 258 | } 259 | 260 | 261 | public function testGetAssetsFromDirectory() 262 | { 263 | $this->assertCount(0, $this->directory->getAssets()); 264 | } 265 | 266 | 267 | public function testGetAssetsFromDirectoryAndChildDirectories() 268 | { 269 | $this->finder->shouldReceive('setWorkingDirectory')->once()->with('css')->andReturn('path/to/public/css'); 270 | $this->finder->shouldReceive('resetWorkingDirectory'); 271 | 272 | $this->directory->directory('css'); 273 | 274 | $this->assertCount(0, $this->directory->getAssets()); 275 | } 276 | 277 | 278 | } -------------------------------------------------------------------------------- /src/Basset/Directory.php: -------------------------------------------------------------------------------- 1 | factory = $factory; 64 | $this->finder = $finder; 65 | $this->path = $path; 66 | $this->assets = new \Illuminate\Support\Collection; 67 | $this->directories = new \Illuminate\Support\Collection; 68 | } 69 | 70 | /** 71 | * Find and add an asset to the directory. 72 | * 73 | * @param string $name 74 | * @param \Closure $callback 75 | * @return \Basset\Asset 76 | */ 77 | public function add($name, Closure $callback = null) 78 | { 79 | try 80 | { 81 | $path = $this->finder->find($name); 82 | 83 | if (isset($this->assets[$path])) 84 | { 85 | $asset = $this->assets[$path]; 86 | } 87 | else 88 | { 89 | $asset = $this->factory->get('asset')->make($path); 90 | 91 | $asset->isRemote() and $asset->raw(); 92 | } 93 | 94 | is_callable($callback) and call_user_func($callback, $asset); 95 | 96 | return $this->assets[$path] = $asset; 97 | } 98 | catch (AssetNotFoundException $e) 99 | { 100 | $this->getLogger()->error(sprintf('Asset "%s" could not be found in "%s"', $name, $this->path)); 101 | 102 | return $this->factory->get('asset')->make(null); 103 | } 104 | } 105 | 106 | /** 107 | * Find and add a javascript asset to the directory. 108 | * 109 | * @param string $name 110 | * @param \Closure $callback 111 | * @return \Basset\Asset 112 | */ 113 | public function javascript($name, Closure $callback = null) 114 | { 115 | return $this->add($name, function($asset) use ($callback) 116 | { 117 | $asset->setGroup('javascripts'); 118 | 119 | is_callable($callback) and call_user_func($callback, $asset); 120 | }); 121 | } 122 | 123 | /** 124 | * Find and add a stylesheet asset to the directory. 125 | * 126 | * @param string $name 127 | * @param \Closure $callback 128 | * @return \Basset\Asset 129 | */ 130 | public function stylesheet($name, Closure $callback = null) 131 | { 132 | return $this->add($name, function($asset) use ($callback) 133 | { 134 | $asset->setGroup('stylesheets'); 135 | 136 | is_callable($callback) and call_user_func($callback, $asset); 137 | }); 138 | } 139 | 140 | /** 141 | * Change the working directory. 142 | * 143 | * @param string $path 144 | * @param \Closure $callback 145 | * @return \Basset\Collection|\Basset\Directory 146 | */ 147 | public function directory($path, Closure $callback = null) 148 | { 149 | try 150 | { 151 | $path = $this->finder->setWorkingDirectory($path); 152 | 153 | $this->directories[$path] = new Directory($this->factory, $this->finder, $path); 154 | 155 | // Once we've set the working directory we'll fire the callback so that any added assets 156 | // are relative to the working directory. After the callback we can revert the working 157 | // directory. 158 | is_callable($callback) and call_user_func($callback, $this->directories[$path]); 159 | 160 | $this->finder->resetWorkingDirectory(); 161 | 162 | return $this->directories[$path]; 163 | } 164 | catch (DirectoryNotFoundException $e) 165 | { 166 | $this->getLogger()->error(sprintf('Directory "%s" could not be found in "%s"', $path, $this->path)); 167 | 168 | return new Directory($this->factory, $this->finder, null); 169 | } 170 | } 171 | 172 | /** 173 | * Recursively iterate through a given path. 174 | * 175 | * @param string $path 176 | * @return \RecursiveIteratorIterator 177 | */ 178 | public function recursivelyIterateDirectory($path) 179 | { 180 | try 181 | { 182 | return new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path)); 183 | } 184 | catch (Exception $e) { return false; } 185 | } 186 | 187 | /** 188 | * Iterate through a given path. 189 | * 190 | * @param string $path 191 | * @return \FilesystemIterator 192 | */ 193 | public function iterateDirectory($path) 194 | { 195 | try 196 | { 197 | return new FilesystemIterator($path); 198 | } 199 | catch (Exception $e) { return false; } 200 | } 201 | 202 | /** 203 | * Require a directory. 204 | * 205 | * @param string $path 206 | * @return \Basset\Directory 207 | */ 208 | public function requireDirectory($path = null) 209 | { 210 | if ( ! is_null($path)) 211 | { 212 | return $this->directory($path)->requireDirectory(); 213 | } 214 | 215 | if ($iterator = $this->iterateDirectory($this->path)) 216 | { 217 | return $this->processRequire($iterator); 218 | } 219 | 220 | return $this; 221 | } 222 | 223 | /** 224 | * Require a directory tree. 225 | * 226 | * @param string $path 227 | * @return \Basset\Directory 228 | */ 229 | public function requireTree($path = null) 230 | { 231 | if ( ! is_null($path)) 232 | { 233 | return $this->directory($path)->requireTree(); 234 | } 235 | 236 | if ($iterator = $this->recursivelyIterateDirectory($this->path)) 237 | { 238 | return $this->processRequire($iterator); 239 | } 240 | 241 | return $this; 242 | } 243 | 244 | /** 245 | * Process a require of either the directory or tree. 246 | * 247 | * @param \Iterator $iterator 248 | * @return \Basset\Directory 249 | */ 250 | protected function processRequire(Iterator $iterator) 251 | { 252 | // Spin through each of the files within the iterator and if their a valid asset they 253 | // are added to the array of assets for this directory. 254 | foreach ($iterator as $file) 255 | { 256 | if ( ! $file->isFile()) continue; 257 | 258 | $this->add($file->getPathname()); 259 | } 260 | 261 | return $this; 262 | } 263 | 264 | /** 265 | * Exclude an array of assets. 266 | * 267 | * @param string|array $assets 268 | * @return \Basset\Directory 269 | */ 270 | public function except($assets) 271 | { 272 | $assets = array_flatten(func_get_args()); 273 | 274 | // Store the directory instance on a variable that we can inject into the scope of 275 | // the closure below. This allows us to call the path conversion method. 276 | $directory = $this; 277 | 278 | $this->assets = $this->assets->filter(function($asset) use ($assets, $directory) 279 | { 280 | $path = $directory->getPathRelativeToDirectory($asset->getRelativePath()); 281 | 282 | return ! in_array($path, $assets); 283 | }); 284 | 285 | return $this; 286 | } 287 | 288 | /** 289 | * Include only a subset of assets. 290 | * 291 | * @param string|array $assets 292 | * @return \Basset\Directory 293 | */ 294 | public function only($assets) 295 | { 296 | $assets = array_flatten(func_get_args()); 297 | 298 | // Store the directory instance on a variable that we can inject into the scope of 299 | // the closure below. This allows us to call the path conversion method. 300 | $directory = $this; 301 | 302 | $this->assets = $this->assets->filter(function($asset) use ($assets, $directory) 303 | { 304 | $path = $directory->getPathRelativeToDirectory($asset->getRelativePath()); 305 | 306 | return in_array($path, $assets); 307 | }); 308 | 309 | return $this; 310 | } 311 | 312 | /** 313 | * Get a path relative from the current directory's path. 314 | * 315 | * @param string $path 316 | * @return string 317 | */ 318 | public function getPathRelativeToDirectory($path) 319 | { 320 | // Get the last segment of the directory as asset paths will be relative to this 321 | // path. We can then replace this segment with nothing in the assets path. 322 | $directoryLastSegment = substr($this->path, strrpos($this->path, '/') + 1); 323 | 324 | return trim(preg_replace('/^'.$directoryLastSegment.'/', '', $path), '/'); 325 | } 326 | 327 | /** 328 | * All assets within directory will be served raw. 329 | * 330 | * @return \Basset\Directory 331 | */ 332 | public function raw() 333 | { 334 | $this->assets->each(function($asset) { $asset->raw(); }); 335 | 336 | return $this; 337 | } 338 | 339 | /** 340 | * All assets within directory will be served raw on a given environment. 341 | * 342 | * @return \Basset\Directory 343 | */ 344 | public function rawOnEnvironment($environment) 345 | { 346 | $this->assets->each(function($asset) use ($environment) { $asset->rawOnEnvironment($environment); }); 347 | 348 | return $this; 349 | } 350 | 351 | /** 352 | * Get the path to the directory. 353 | * 354 | * @return string 355 | */ 356 | public function getPath() 357 | { 358 | return $this->path; 359 | } 360 | 361 | /** 362 | * Get all the assets. 363 | * 364 | * @return \Illuminate\Support\Collection 365 | */ 366 | public function getAssets() 367 | { 368 | $assets = $this->assets; 369 | 370 | // Spin through each directory and recursively merge the current directories assets 371 | // on to the directories assets. This maintains the order of adding in the array 372 | // structure. 373 | $this->directories->each(function($directory) use (&$assets) 374 | { 375 | $assets = $directory->getAssets()->merge($assets); 376 | }); 377 | 378 | // Spin through each of the filters and apply them to each of the assets. Every filter 379 | // is applied and then later during the build will be removed if it does not apply 380 | // to a given asset. 381 | $this->filters->each(function($filter) use (&$assets) 382 | { 383 | $assets->each(function($asset) use ($filter) { $asset->apply($filter); }); 384 | }); 385 | 386 | return $assets; 387 | } 388 | 389 | /** 390 | * Get the current directories assets. 391 | * 392 | * @return \Illuminate\Support\Collection 393 | */ 394 | public function getDirectoryAssets() 395 | { 396 | return $this->assets; 397 | } 398 | 399 | } -------------------------------------------------------------------------------- /tests/Basset/Builder/BuilderTest.php: -------------------------------------------------------------------------------- 1 | files = m::mock('Illuminate\Filesystem\Filesystem'); 17 | $this->files->shouldReceive('exists')->once()->with('foo')->andReturn(true); 18 | $this->manifest = m::mock('Basset\Manifest\Manifest'); 19 | $this->collection = m::mock('Basset\Collection'); 20 | $this->builder = new Basset\Builder\Builder($this->files, $this->manifest, 'foo'); 21 | } 22 | 23 | 24 | public function testBuilderChecksForBuildPathAndMakesDirectoryIfItDoesNotExist() 25 | { 26 | $this->files->shouldReceive('exists')->once()->with('foo')->andReturn(false); 27 | $this->files->shouldReceive('makeDirectory')->once()->with('foo')->andReturn(true); 28 | 29 | $builder = new Basset\Builder\Builder($this->files, $this->manifest, 'foo'); 30 | } 31 | 32 | 33 | /** 34 | * @expectedException Basset\Exceptions\BuildNotRequiredException 35 | */ 36 | public function testBuildingEmptyProductionCollectionThrowsBuildNotRequiredException() 37 | { 38 | $collection = m::mock('Basset\Collection'); 39 | $collection->shouldReceive('getAssetsWithoutRaw')->once()->with('stylesheets')->andReturn(new Illuminate\Support\Collection); 40 | $collection->shouldReceive('getIdentifier')->once()->andReturn('foo'); 41 | 42 | $this->manifest->shouldReceive('make')->once()->with('foo')->andReturn($entry = m::mock('Basset\Manifest\Entry')); 43 | $entry->shouldReceive('resetProductionFingerprint')->once()->with('stylesheets'); 44 | 45 | $this->builder->buildAsProduction($collection, 'stylesheets'); 46 | } 47 | 48 | 49 | /** 50 | * @expectedException Basset\Exceptions\BuildNotRequiredException 51 | */ 52 | public function testBuildingExistingProductionCollectionThrowsBuildNotRequiredException() 53 | { 54 | $this->collection->shouldReceive('getAssetsWithoutRaw')->once()->with('stylesheets')->andReturn(new Illuminate\Support\Collection(array( 55 | $asset = m::mock('Basset\Asset') 56 | ))); 57 | $asset->shouldReceive('build')->once()->andReturn('body { }'); 58 | 59 | $this->collection->shouldReceive('getIdentifier')->once()->andReturn('foo'); 60 | $this->collection->shouldReceive('getExtension')->once()->with('stylesheets')->andReturn('css'); 61 | 62 | $this->manifest->shouldReceive('make')->once()->with('foo')->andReturn($entry = m::mock('Basset\Manifest\Entry')); 63 | $entry->shouldReceive('getProductionFingerprint')->with('stylesheets')->andReturn($fingerprint = 'foo-'.md5('body { }').'.css'); 64 | 65 | $this->files->shouldReceive('exists')->once()->with('foo/'.$fingerprint)->andReturn(true); 66 | 67 | $this->builder->buildAsProduction($this->collection, 'stylesheets'); 68 | } 69 | 70 | 71 | public function testBuildingProductionCollectionWritesToFilesystemAndSetsProductionFingerprint() 72 | { 73 | $this->collection->shouldReceive('getAssetsWithoutRaw')->once()->with('stylesheets')->andReturn(new Illuminate\Support\Collection(array( 74 | $asset = m::mock('Basset\Asset') 75 | ))); 76 | $asset->shouldReceive('build')->once()->andReturn('body { }'); 77 | 78 | $this->collection->shouldReceive('getIdentifier')->once()->andReturn('foo'); 79 | $this->collection->shouldReceive('getExtension')->once()->with('stylesheets')->andReturn('css'); 80 | 81 | $fingerprint = 'foo-'.md5('body { }').'.css'; 82 | 83 | $this->manifest->shouldReceive('make')->once()->with('foo')->andReturn($entry = m::mock('Basset\Manifest\Entry')); 84 | $entry->shouldReceive('getProductionFingerprint')->with('stylesheets')->andReturn(null); 85 | $entry->shouldReceive('setProductionFingerprint')->with('stylesheets', $fingerprint); 86 | 87 | $this->files->shouldReceive('put')->once()->with('foo/'.$fingerprint, 'body { }'); 88 | 89 | $this->builder->buildAsProduction($this->collection, 'stylesheets'); 90 | } 91 | 92 | 93 | public function testBuildingProductionCollectionWithForce() 94 | { 95 | $this->collection->shouldReceive('getAssetsWithoutRaw')->once()->with('stylesheets')->andReturn(new Illuminate\Support\Collection(array( 96 | $asset = m::mock('Basset\Asset') 97 | ))); 98 | $asset->shouldReceive('build')->once()->andReturn('body { }'); 99 | 100 | $this->collection->shouldReceive('getIdentifier')->once()->andReturn('foo'); 101 | $this->collection->shouldReceive('getExtension')->once()->with('stylesheets')->andReturn('css'); 102 | 103 | $fingerprint = 'foo-'.md5('body { }').'.css'; 104 | 105 | $this->manifest->shouldReceive('make')->once()->with('foo')->andReturn($entry = m::mock('Basset\Manifest\Entry')); 106 | $entry->shouldReceive('getProductionFingerprint')->with('stylesheets')->andReturn($fingerprint); 107 | $entry->shouldReceive('setProductionFingerprint')->with('stylesheets', $fingerprint); 108 | 109 | $this->files->shouldReceive('put')->once()->with('foo/'.$fingerprint, 'body { }'); 110 | 111 | $this->builder->setForce(true); 112 | $this->builder->buildAsProduction($this->collection, 'stylesheets'); 113 | } 114 | 115 | 116 | public function testBuildingProductionCollectionWithGzip() 117 | { 118 | $this->collection->shouldReceive('getAssetsWithoutRaw')->once()->with('stylesheets')->andReturn(new Illuminate\Support\Collection(array( 119 | $asset = m::mock('Basset\Asset') 120 | ))); 121 | $asset->shouldReceive('build')->once()->andReturn('body { }'); 122 | 123 | $this->collection->shouldReceive('getIdentifier')->once()->andReturn('foo'); 124 | $this->collection->shouldReceive('getExtension')->once()->with('stylesheets')->andReturn('css'); 125 | 126 | $fingerprint = 'foo-'.md5('body { }').'.css'; 127 | 128 | $this->manifest->shouldReceive('make')->once()->with('foo')->andReturn($entry = m::mock('Basset\Manifest\Entry')); 129 | $entry->shouldReceive('getProductionFingerprint')->with('stylesheets')->andReturn(null); 130 | $entry->shouldReceive('setProductionFingerprint')->with('stylesheets', $fingerprint); 131 | 132 | $this->files->shouldReceive('put')->once()->with('foo/'.$fingerprint, gzencode('body { }', 9)); 133 | 134 | $this->builder->setGzip(true); 135 | $this->builder->buildAsProduction($this->collection, 'stylesheets'); 136 | } 137 | 138 | 139 | /** 140 | * @expectedException Basset\Exceptions\BuildNotRequiredException 141 | */ 142 | public function testBuildingDevelopmentCollectionWithNoAssetsThrowsBuildNotRequiredException() 143 | { 144 | $this->collection->shouldReceive('getAssetsWithoutRaw')->once()->with('stylesheets')->andReturn(new Illuminate\Support\Collection); 145 | $this->collection->shouldReceive('getIdentifier')->once()->andReturn('foo'); 146 | 147 | $this->manifest->shouldReceive('make')->once()->with('foo')->andReturn($entry = m::mock('Basset\Manifest\Entry')); 148 | $entry->shouldReceive('hasDevelopmentAssets')->once()->with('stylesheets')->andReturn(false); 149 | $entry->shouldReceive('resetDevelopmentAssets')->once()->with('stylesheets'); 150 | 151 | $this->builder->buildAsDevelopment($this->collection, 'stylesheets'); 152 | } 153 | 154 | 155 | /** 156 | * @expectedException Basset\Exceptions\BuildNotRequiredException 157 | */ 158 | public function testBuildingDevelopmentCollectionWithAssetsThatAreAlreadyBuiltThrowsBuildNotRequiredException() 159 | { 160 | $this->collection->shouldReceive('getAssetsWithoutRaw')->once()->with('stylesheets')->andReturn(new Illuminate\Support\Collection(array( 161 | $assets[] = m::mock('Basset\Asset'), 162 | $assets[] = m::mock('Basset\Asset') 163 | ))); 164 | $this->collection->shouldReceive('getIdentifier')->once()->andReturn('foo'); 165 | 166 | $assets[0]->shouldReceive('getRelativePath')->once()->andReturn('bar/baz.css'); 167 | $assets[0]->shouldReceive('getBuildPath')->once()->andReturn('bar/baz-123.css'); 168 | $assets[1]->shouldReceive('getRelativePath')->once()->andReturn('bar/qux.css'); 169 | $assets[1]->shouldReceive('getBuildPath')->once()->andReturn('bar/qux-321.css'); 170 | 171 | $this->manifest->shouldReceive('make')->once()->with('foo')->andReturn($entry = m::mock('Basset\Manifest\Entry')); 172 | $entry->shouldReceive('hasDevelopmentAsset')->once()->with($assets[0])->andReturn(true); 173 | $entry->shouldReceive('getDevelopmentAsset')->once()->with($assets[0])->andReturn('bar/baz-123.css'); 174 | $entry->shouldReceive('hasDevelopmentAsset')->once()->with($assets[1])->andReturn(true); 175 | $entry->shouldReceive('getDevelopmentAsset')->once()->with($assets[1])->andReturn('bar/qux-321.css'); 176 | 177 | $entry->shouldReceive('hasDevelopmentAssets')->once()->with('stylesheets')->andReturn(true); 178 | $entry->shouldReceive('getDevelopmentAssets')->once()->with('stylesheets')->andReturn(array( 179 | 'bar/baz.css' => 'bar/baz-123.css', 180 | 'bar/qux.css' => 'bar/qux-321.css' 181 | )); 182 | 183 | $this->builder->buildAsDevelopment($this->collection, 'stylesheets'); 184 | } 185 | 186 | 187 | public function testBuildingDevelopmentCollectionWithNoCurrentManifestEntry() 188 | { 189 | $this->collection->shouldReceive('getAssetsWithoutRaw')->once()->with('stylesheets')->andReturn(new Illuminate\Support\Collection(array( 190 | $assets[] = m::mock('Basset\Asset'), 191 | $assets[] = m::mock('Basset\Asset') 192 | ))); 193 | $this->collection->shouldReceive('getIdentifier')->once()->andReturn('foo'); 194 | 195 | $this->manifest->shouldReceive('make')->once()->with('foo')->andReturn($entry = m::mock('Basset\Manifest\Entry')); 196 | $entry->shouldReceive('hasDevelopmentAssets')->once()->with('stylesheets')->andReturn(false); 197 | $entry->shouldReceive('resetDevelopmentAssets')->once()->with('stylesheets'); 198 | 199 | $assets[0]->shouldReceive('getBuildPath')->once()->andReturn('bar/baz-123.css'); 200 | $assets[0]->shouldReceive('build')->once()->andReturn('body { }'); 201 | $assets[1]->shouldReceive('getBuildPath')->once()->andReturn('bar/qux-321.css'); 202 | $assets[1]->shouldReceive('build')->once()->andReturn('html { }'); 203 | 204 | $entry->shouldReceive('addDevelopmentAsset')->once()->with($assets[0]); 205 | $entry->shouldReceive('addDevelopmentAsset')->once()->with($assets[1]); 206 | 207 | $this->files->shouldReceive('exists')->once()->with('foo/foo/bar')->andReturn(false); 208 | $this->files->shouldReceive('exists')->once()->with('foo/foo/bar')->andReturn(true); 209 | $this->files->shouldReceive('makeDirectory')->once()->with('foo/foo/bar', 0777, true); 210 | 211 | $this->files->shouldReceive('put')->once()->with('foo/foo/bar/baz-123.css', 'body { }'); 212 | $this->files->shouldReceive('put')->once()->with('foo/foo/bar/qux-321.css', 'html { }'); 213 | 214 | $this->builder->buildAsDevelopment($this->collection, 'stylesheets'); 215 | } 216 | 217 | 218 | public function testBuildingDevelopmentCollectionWithNoChangesButWithForcing() 219 | { 220 | $this->collection->shouldReceive('getAssetsWithoutRaw')->once()->with('stylesheets')->andReturn(new Illuminate\Support\Collection(array( 221 | $assets[] = m::mock('Basset\Asset'), 222 | $assets[] = m::mock('Basset\Asset') 223 | ))); 224 | $this->collection->shouldReceive('getIdentifier')->once()->andReturn('foo'); 225 | 226 | $assets[0]->shouldReceive('getRelativePath')->once()->andReturn('bar/baz.css'); 227 | $assets[0]->shouldReceive('getBuildPath')->once()->andReturn('bar/baz-123.css'); 228 | $assets[0]->shouldReceive('build')->once()->andReturn('body { }'); 229 | $assets[1]->shouldReceive('getRelativePath')->once()->andReturn('bar/qux.css'); 230 | $assets[1]->shouldReceive('getBuildPath')->once()->andReturn('bar/qux-321.css'); 231 | $assets[1]->shouldReceive('build')->once()->andReturn('html { }'); 232 | 233 | $this->manifest->shouldReceive('make')->once()->with('foo')->andReturn($entry = m::mock('Basset\Manifest\Entry')); 234 | $entry->shouldReceive('hasDevelopmentAssets')->once()->with('stylesheets')->andReturn(true); 235 | $entry->shouldReceive('resetDevelopmentAssets')->once()->with('stylesheets'); 236 | $entry->shouldReceive('getDevelopmentAssets')->once()->with('stylesheets')->andReturn(array( 237 | 'bar/baz.css' => 'bar/baz-123.css', 238 | 'bar/qux.css' => 'bar/qux-321.css' 239 | )); 240 | 241 | $entry->shouldReceive('addDevelopmentAsset')->once()->with($assets[0]); 242 | $entry->shouldReceive('addDevelopmentAsset')->once()->with($assets[1]); 243 | 244 | $this->files->shouldReceive('exists')->once()->with('foo/foo/bar')->andReturn(false); 245 | $this->files->shouldReceive('exists')->once()->with('foo/foo/bar')->andReturn(true); 246 | $this->files->shouldReceive('makeDirectory')->once()->with('foo/foo/bar', 0777, true); 247 | 248 | $this->files->shouldReceive('put')->once()->with('foo/foo/bar/baz-123.css', 'body { }'); 249 | $this->files->shouldReceive('put')->once()->with('foo/foo/bar/qux-321.css', 'html { }'); 250 | 251 | $this->builder->setForce(true); 252 | $this->builder->buildAsDevelopment($this->collection, 'stylesheets'); 253 | } 254 | 255 | 256 | public function testBuildingDevelopmentCollectionWithGzip() 257 | { 258 | $this->collection->shouldReceive('getAssetsWithoutRaw')->once()->with('stylesheets')->andReturn(new Illuminate\Support\Collection(array( 259 | $asset = m::mock('Basset\Asset') 260 | ))); 261 | $this->collection->shouldReceive('getIdentifier')->once()->andReturn('foo'); 262 | 263 | $asset->shouldReceive('getRelativePath')->once()->andReturn('bar/baz.css'); 264 | $asset->shouldReceive('getBuildPath')->once()->andReturn('bar/baz-123.css'); 265 | $asset->shouldReceive('build')->once()->andReturn('body { }'); 266 | 267 | $this->manifest->shouldReceive('make')->once()->with('foo')->andReturn($entry = m::mock('Basset\Manifest\Entry')); 268 | $entry->shouldReceive('hasDevelopmentAssets')->once()->with('stylesheets')->andReturn(true); 269 | $entry->shouldReceive('getDevelopmentAssets')->once()->with('stylesheets')->andReturn(array('bar/baz.css' => 'bar/baz-123.css')); 270 | 271 | $entry->shouldReceive('hasDevelopmentAsset')->once()->with($asset)->andReturn(false); 272 | $entry->shouldReceive('addDevelopmentAsset')->once()->with($asset); 273 | 274 | $this->files->shouldReceive('exists')->once()->with('foo/foo/bar')->andReturn(true); 275 | 276 | $this->files->shouldReceive('put')->once()->with('foo/foo/bar/baz-123.css', gzencode('body { }', 9)); 277 | 278 | $this->builder->setGzip(true); 279 | $this->builder->buildAsDevelopment($this->collection, 'stylesheets'); 280 | } 281 | 282 | 283 | } --------------------------------------------------------------------------------