├── .gitignore ├── .php_cs.dist ├── .travis.yml ├── .travis ├── after_script.php ├── before_install.php ├── common.php ├── install.php └── script.php ├── CONTRIBUTING.md ├── Command ├── Options │ ├── BaseOptionHelper.php │ ├── ChoiceOptionHelper.php │ └── SimpleOptionHelper.php └── SetupCommand.php ├── DependencyInjection ├── AssetExtensionLoader.php ├── Compiler │ └── AssetCompilerPass.php ├── Configuration.php └── RjFrontendExtension.php ├── EventListener └── InjectLiveReloadListener.php ├── LICENSE ├── Manifest ├── Loader │ ├── AbstractManifestLoader.php │ ├── CachedManifestLoader.php │ ├── JsonManifestLoader.php │ └── ManifestLoaderInterface.php └── Manifest.php ├── Package └── FallbackPackage.php ├── README.md ├── Resources ├── blueprints │ ├── bower.json.php │ ├── images │ │ └── .keep │ ├── pipelines │ │ └── gulp │ │ │ ├── gulpfile.js.php │ │ │ └── package.json.php │ ├── scripts │ │ ├── coffee │ │ │ └── app.coffee │ │ └── js │ │ │ └── app.js │ └── stylesheets │ │ ├── less │ │ ├── app.less.php │ │ └── vendor.less.php │ │ ├── none │ │ └── app.css.php │ │ └── sass │ │ ├── app.scss.php │ │ └── vendor.scss.php ├── config │ ├── asset.yml │ ├── commands.yml │ ├── console.yml │ ├── fallback.yml │ ├── livereload.yml │ ├── manifest.yml │ └── version_strategy.yml └── doc │ ├── _static │ └── custom.css │ ├── bower.rst │ ├── conf.py │ ├── deployment.rst │ ├── directory-structure.rst │ ├── index.rst │ ├── referencing-assets.rst │ └── setup.rst ├── RjFrontendBundle.php ├── Tests ├── Command │ └── SetupCommandTest.php ├── DependencyInjection │ ├── Compiler │ │ ├── BaseCompilerPassTest.php │ │ └── Packages │ │ │ └── AssetCompilerPassTest.php │ ├── ConfigurationTest.php │ ├── RjFrontendExtensionAssetTest.php │ ├── RjFrontendExtensionBaseTest.php │ └── RjFrontendExtensionTest.php ├── EventListener │ └── InjectLiveReloadListenerTest.php ├── Functional │ ├── BaseTestCase.php │ ├── InjectLivereloadTest.php │ ├── PackagesTest.php │ └── TestApp │ │ ├── TestBundle │ │ ├── Controller │ │ │ ├── InjectLivereloadController.php │ │ │ └── PackagesController.php │ │ ├── Resources │ │ │ └── views │ │ │ │ └── Packages │ │ │ │ ├── custom.html.php │ │ │ │ ├── default.html.php │ │ │ │ └── fallback.html.php │ │ └── TestBundle.php │ │ ├── app │ │ ├── AppKernel.php │ │ └── config │ │ │ ├── config.yml │ │ │ └── routing.yml │ │ └── web │ │ └── assets │ │ └── manifest.json ├── Manifest │ ├── Loader │ │ ├── CachedManifestLoaderTest.php │ │ └── JsonManifestLoaderTest.php │ └── ManifestTest.php ├── Package │ └── FallbackPackageTest.php └── VersionStrategy │ └── ManifestVersionStrategyTest.php ├── Util └── Util.php ├── VersionStrategy ├── EmptyVersionStrategy.php └── ManifestVersionStrategy.php ├── composer.json ├── phpunit.xml.dist └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /bin/ 3 | /venv/ 4 | /Resources/doc/_build/ 5 | composer.lock 6 | .php_cs.cache 7 | -------------------------------------------------------------------------------- /.php_cs.dist: -------------------------------------------------------------------------------- 1 | in(__DIR__) 5 | ->depth('> 0') 6 | ->exclude('venv') 7 | ->exclude('Resources') 8 | ; 9 | 10 | return PhpCsFixer\Config::create() 11 | ->setRules([ 12 | '@Symfony' => true, 13 | 'array_syntax' => ['syntax' => 'short'], 14 | ]) 15 | ->setFinder($finder) 16 | ; 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | branches: 4 | only: 5 | - master 6 | 7 | cache: 8 | directories: 9 | - vendor 10 | - venv 11 | 12 | before_install: ./.travis/before_install.php 13 | install: ./.travis/install.php 14 | script: ./.travis/script.php 15 | after_script: ./.travis/after_script.php 16 | 17 | env: 18 | global: 19 | - SYMFONY_DEPRECATIONS_HELPER=weak 20 | 21 | matrix: 22 | include: 23 | # old PHP versions 24 | - php: 5.4 25 | env: SYMFONY_VERSION=2.8.* # Symfony 3 doesn't support PHP 5.4 26 | - php: 5.6 27 | env: SYMFONY_VERSION=3.4.* 28 | # current PHP with all non-EOLed Symfony versions 29 | - php: 7.2 30 | env: SYMFONY_VERSION=2.7.* 31 | - php: 7.2 32 | env: SYMFONY_VERSION=2.8.* 33 | - php: 7.2 34 | env: SYMFONY_VERSION=3.2.* 35 | - php: 7.2 36 | env: SYMFONY_VERSION=3.3.* 37 | - php: 7.2 38 | env: SYMFONY_VERSION=3.4.* 39 | - php: 7.2 40 | env: SYMFONY_VERSION=dev-master 41 | allow_failures: 42 | - env: SYMFONY_VERSION=dev-master 43 | -------------------------------------------------------------------------------- /.travis/after_script.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | command = $command; 57 | $this->input = $input; 58 | $this->output = $output; 59 | } 60 | 61 | /** 62 | * @param array $allowedValues 63 | * 64 | * @return $this 65 | */ 66 | public function setAllowedValues(array $allowedValues) 67 | { 68 | $this->allowedValues = $allowedValues; 69 | 70 | return $this; 71 | } 72 | 73 | /** 74 | * @param string $errorMessage 75 | * 76 | * @return $this 77 | */ 78 | public function setErrorMessage($errorMessage) 79 | { 80 | $this->errorMessage = $errorMessage; 81 | 82 | return $this; 83 | } 84 | 85 | /** 86 | * @param mixed $defaultValue 87 | * 88 | * @return $this 89 | */ 90 | public function setDefaultValue($defaultValue) 91 | { 92 | $this->defaultValue = $defaultValue; 93 | 94 | return $this; 95 | } 96 | 97 | /** 98 | * @param string $name 99 | * @param string $question 100 | * 101 | * @return string|bool 102 | */ 103 | public function setOption($name, $question) 104 | { 105 | $selection = $this->input->getOption($name); 106 | $allowed = $this->allowedValues; 107 | $error = $this->errorMessage; 108 | 109 | if (null !== $selection && !empty($allowed) && !in_array($selection, $allowed)) { 110 | $this->output->writeln(sprintf("$error", $selection)); 111 | $selection = null; 112 | } 113 | 114 | if (null === $selection) { 115 | $selection = $this->ask($this->getQuestion($question)); 116 | } 117 | 118 | if ($selection === 'false') { 119 | $selection = false; 120 | } 121 | 122 | if ($selection === 'true') { 123 | $selection = true; 124 | } 125 | 126 | $this->input->setOption($name, $selection); 127 | 128 | return $selection; 129 | } 130 | 131 | /** 132 | * @param Question $question 133 | * 134 | * @return mixed 135 | */ 136 | private function ask(Question $question) 137 | { 138 | return $this->command->getHelper('question')->ask($this->input, $this->output, $question); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Command/Options/ChoiceOptionHelper.php: -------------------------------------------------------------------------------- 1 | $question", $this->allowedValues, $this->defaultValue); 15 | $question->setErrorMessage($this->errorMessage); 16 | 17 | return $question; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Command/Options/SimpleOptionHelper.php: -------------------------------------------------------------------------------- 1 | $question", $this->defaultValue); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Command/SetupCommand.php: -------------------------------------------------------------------------------- 1 | templating = new PhpEngine( 36 | new TemplateNameParser(), 37 | new FilesystemLoader([__DIR__.'/../Resources/blueprints/%name%']) 38 | ); 39 | } 40 | 41 | /** 42 | * @param string $path 43 | */ 44 | public function setRootDir($path) 45 | { 46 | $this->rootDir = $path; 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | */ 52 | protected function configure() 53 | { 54 | $this 55 | ->setName('rj_frontend:setup') 56 | ->setDescription('Generate the configuration for the asset pipeline') 57 | ->addOption( 58 | 'dry-run', 59 | null, 60 | InputOption::VALUE_NONE, 61 | 'Output which commands would have been run instead of running them' 62 | ) 63 | ->addOption( 64 | 'force', 65 | null, 66 | InputOption::VALUE_NONE, 67 | 'Force execution' 68 | ) 69 | ->addOption( 70 | 'src-dir', 71 | null, 72 | InputOption::VALUE_REQUIRED, 73 | 'Path to the directory containing the source assets [e.g. '.$this->getDefaultOption('src-dir').']' 74 | ) 75 | ->addOption( 76 | 'dest-dir', 77 | null, 78 | InputOption::VALUE_REQUIRED, 79 | 'Path to the directory containing the compiled assets [e.g. '.$this->getDefaultOption('dest-dir').']' 80 | ) 81 | ->addOption( 82 | 'pipeline', 83 | null, 84 | InputOption::VALUE_REQUIRED, 85 | 'Asset pipeline to use [only gulp is available at the moment]' 86 | ) 87 | ->addOption( 88 | 'csspre', 89 | null, 90 | InputOption::VALUE_REQUIRED, 91 | 'CSS preprocessor to use [sass, less or none]' 92 | ) 93 | ->addOption( 94 | 'coffee', 95 | null, 96 | InputOption::VALUE_REQUIRED, 97 | 'Use the CoffeeScript compiler [true or false]' 98 | ) 99 | ; 100 | } 101 | 102 | /** 103 | * {@inheritdoc} 104 | */ 105 | protected function interact(InputInterface $input, OutputInterface $output) 106 | { 107 | $simpleOptionHelper = new SimpleOptionHelper($this, $input, $output); 108 | $choiceOptionHelper = new ChoiceOptionHelper($this, $input, $output); 109 | 110 | $simpleOptionHelper 111 | ->setDefaultValue($this->getDefaultOption('src-dir')) 112 | ->setOption( 113 | 'src-dir', 114 | 'Path to the directory containing the source assets [default is '.$this->getDefaultOption('src-dir').']' 115 | ) 116 | ; 117 | 118 | $simpleOptionHelper 119 | ->setDefaultValue($this->getDefaultOption('dest-dir')) 120 | ->setOption( 121 | 'dest-dir', 122 | 'Path to the directory containing the compiled assets [default is '.$this->getDefaultOption('dest-dir').']' 123 | ) 124 | ; 125 | 126 | $choiceOptionHelper 127 | ->setAllowedValues(['gulp']) 128 | ->setErrorMessage('%s is not a supported asset pipeline') 129 | ->setOption( 130 | 'pipeline', 131 | 'Asset pipeline to use [only gulp is available at the moment]' 132 | ) 133 | ; 134 | 135 | $choiceOptionHelper 136 | ->setAllowedValues(['sass', 'less', 'none']) 137 | ->setErrorMessage('%s is not a supported CSS preprocessor') 138 | ->setOption( 139 | 'csspre', 140 | 'CSS preprocessor to use [default is '.$this->getDefaultOption('csspre').']' 141 | ) 142 | ; 143 | 144 | $choiceOptionHelper 145 | ->setAllowedValues(['false', 'true']) 146 | ->setErrorMessage('%s is not a supported value for --coffee. Use either true or false') 147 | ->setOption( 148 | 'coffee', 149 | 'Whether to use the CoffeeScript compiler [default is '.$this->getDefaultOption('coffee').']' 150 | ) 151 | ; 152 | 153 | $output->writeln(''); 154 | } 155 | 156 | /** 157 | * {@inheritdoc} 158 | */ 159 | protected function execute(InputInterface $input, OutputInterface $output) 160 | { 161 | $this->processOptions($input); 162 | 163 | $output->writeln('Selected options are:'); 164 | $output->writeln('src-dir: '.$input->getOption('src-dir')); 165 | $output->writeln('dest-dir: '.$input->getOption('dest-dir')); 166 | $output->writeln('pipeline: '.$input->getOption('pipeline')); 167 | $output->writeln('csspre: '.$input->getOption('csspre')); 168 | $output->writeln('coffee: '.($input->getOption('coffee') ? 'true' : 'false')); 169 | 170 | if (!preg_match('|web/.+|', $input->getOption('dest-dir'))) { 171 | throw new \InvalidArgumentException("'dest-dir' must be a directory under web/"); 172 | } 173 | 174 | $output->writeln(''); 175 | $this->createSourceTree($input, $output); 176 | $this->createBuildFile($input, $output); 177 | $this->createPackageJson($input, $output); 178 | $this->createBowerJson($input, $output); 179 | 180 | $output->writeln(''); 181 | $output->writeln('All files generated.'); 182 | $output->writeln(''); 183 | $output->writeln('Make sure to install dependencies:'); 184 | $output->writeln(''); 185 | $output->writeln(' npm install'); 186 | $output->writeln(''); 187 | } 188 | 189 | /** 190 | * @param InputInterface $input 191 | * @param OutputInterface $output 192 | */ 193 | private function createSourceTree(InputInterface $input, OutputInterface $output) 194 | { 195 | $blueprints = __DIR__.'/../Resources/blueprints'; 196 | $dryRun = $input->getOption('dry-run'); 197 | $base = $input->getOption('src-dir'); 198 | 199 | $output->writeln($dryRun 200 | ? 'Would have created directory tree for source assets:' 201 | : 'Creating directory tree for source assets:' 202 | ); 203 | 204 | $blueprintDir = "$blueprints/images"; 205 | $this->createDirFromBlueprint($input, $output, $blueprintDir, "$base/images"); 206 | 207 | $blueprintDir = "$blueprints/stylesheets/".$input->getOption('csspre'); 208 | $this->createDirFromBlueprint($input, $output, $blueprintDir, "$base/stylesheets"); 209 | 210 | $blueprintDir = "$blueprints/scripts/"; 211 | $blueprintDir .= $input->getOption('coffee') ? 'coffee' : 'js'; 212 | $this->createDirFromBlueprint($input, $output, $blueprintDir, "$base/scripts"); 213 | 214 | $output->writeln(''); 215 | } 216 | 217 | /** 218 | * @param InputInterface $input 219 | * @param OutputInterface $output 220 | */ 221 | private function createBuildFile(InputInterface $input, OutputInterface $output) 222 | { 223 | $files = [ 224 | 'gulp' => 'gulp/gulpfile.js', 225 | ]; 226 | 227 | $this->createFileFromTemplate($input, $output, 'pipelines/'.$files[$input->getOption('pipeline')]); 228 | } 229 | 230 | /** 231 | * @param InputInterface $input 232 | * @param OutputInterface $output 233 | */ 234 | private function createPackageJson(InputInterface $input, OutputInterface $output) 235 | { 236 | $files = [ 237 | 'gulp' => 'gulp/package.json', 238 | ]; 239 | 240 | $this->createFileFromTemplate($input, $output, 'pipelines/'.$files[$input->getOption('pipeline')]); 241 | } 242 | 243 | /** 244 | * @param InputInterface $input 245 | * @param OutputInterface $output 246 | */ 247 | private function createBowerJson(InputInterface $input, OutputInterface $output) 248 | { 249 | $this->createFileFromTemplate($input, $output, 'bower.json'); 250 | } 251 | 252 | /** 253 | * @param InputInterface $input 254 | * @param OutputInterface $output 255 | * @param string $blueprintDir 256 | * @param string $targetDir 257 | */ 258 | private function createDirFromBlueprint(InputInterface $input, OutputInterface $output, $blueprintDir, $targetDir) 259 | { 260 | $dryRun = $input->getOption('dry-run'); 261 | 262 | if (!$dryRun && !file_exists($targetDir)) { 263 | mkdir($targetDir, 0777, true); 264 | } 265 | 266 | foreach (preg_grep('/^\.?\w+/', scandir($blueprintDir)) as $entry) { 267 | $target = $entry; 268 | 269 | $isPhpTemplate = substr($entry, strrpos($entry, '.')) === '.php'; 270 | if ($isPhpTemplate) { 271 | $entry = str_replace('.php', '', $entry); 272 | $target = str_replace('.php', '', $target); 273 | } 274 | 275 | $entry = $blueprintDir.'/'.$entry; 276 | $target = $targetDir.'/'.$target; 277 | 278 | if (!$dryRun) { 279 | if ($isPhpTemplate) { 280 | $this->renderTemplate($input, $output, $entry, $target); 281 | } else { 282 | if (file_exists($target) && !$input->getOption('force')) { 283 | $output->writeln( 284 | "$target already exists. Run this command with --force to overwrite 285 | "); 286 | 287 | continue; 288 | } 289 | 290 | copy($entry, $target); 291 | } 292 | } 293 | 294 | $output->writeln($target); 295 | } 296 | } 297 | 298 | /** 299 | * @param InputInterface $input 300 | * @param OutputInterface $output 301 | * @param string $file 302 | */ 303 | private function createFileFromTemplate(InputInterface $input, OutputInterface $output, $file) 304 | { 305 | $dryRun = $input->getOption('dry-run'); 306 | 307 | $targetFile = basename($file); 308 | if (!empty($this->rootDir)) { 309 | $targetFile = $this->rootDir.'/'.$targetFile; 310 | } 311 | 312 | $output->writeln($dryRun 313 | ? "Would have created file $targetFile" 314 | : "Creating file $targetFile" 315 | ); 316 | 317 | if ($dryRun) { 318 | return; 319 | } 320 | 321 | $this->renderTemplate($input, $output, $file, $targetFile); 322 | } 323 | 324 | /** 325 | * @param InputInterface $input 326 | * @param OutputInterface $output 327 | * @param string $file 328 | * @param string $target 329 | */ 330 | private function renderTemplate(InputInterface $input, OutputInterface $output, $file, $target) 331 | { 332 | if (file_exists($target) && !$input->getOption('force')) { 333 | $output->writeln( 334 | "$target already exists. Run this command with --force to overwrite" 335 | ); 336 | } 337 | 338 | switch ($input->getOption('csspre')) { 339 | case 'sass': 340 | $stylesheetExtension = 'scss'; 341 | break; 342 | case 'less': 343 | $stylesheetExtension = 'less'; 344 | break; 345 | default: 346 | $stylesheetExtension = 'css'; 347 | break; 348 | } 349 | 350 | file_put_contents($target, $this->templating->render("$file.php", [ 351 | 'projectName' => basename(getcwd()), 352 | 'srcDir' => $input->getOption('src-dir'), 353 | 'destDir' => $input->getOption('dest-dir'), 354 | 'prefix' => str_replace('web/', '', $input->getOption('dest-dir')), 355 | 'coffee' => $input->getOption('coffee'), 356 | 'cssPre' => $input->getOption('csspre'), 357 | 'stylesheetExtension' => $stylesheetExtension, 358 | ])); 359 | } 360 | 361 | /** 362 | * @param InputInterface $input 363 | */ 364 | private function processOptions(InputInterface $input) 365 | { 366 | foreach ($input->getOptions() as $name => $value) { 367 | if (!$input->isInteractive() && $value === null) { 368 | $value = $this->getDefaultOption($name); 369 | } 370 | 371 | if ($value === 'true') { 372 | $value = true; 373 | } elseif ($value === 'false') { 374 | $value = false; 375 | } 376 | 377 | $input->setOption($name, $value); 378 | } 379 | } 380 | 381 | /** 382 | * @param string $name 383 | * 384 | * @return string 385 | */ 386 | private function getDefaultOption($name) 387 | { 388 | $defaults = [ 389 | 'src-dir' => empty($this->rootDir) ? 'app/Resources' : $this->rootDir.'/app/Resources', 390 | 'dest-dir' => empty($this->rootDir) ? 'web/assets' : $this->rootDir.'/web/assets', 391 | 'pipeline' => 'gulp', 392 | 'csspre' => 'sass', 393 | 'coffee' => 'false', 394 | ]; 395 | 396 | return $defaults[$name]; 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /DependencyInjection/AssetExtensionLoader.php: -------------------------------------------------------------------------------- 1 | alias = $alias; 38 | $this->container = $container; 39 | } 40 | 41 | /** 42 | * @param array $config 43 | * @param LoaderInterface $loader 44 | */ 45 | public function load(array $config, LoaderInterface $loader) 46 | { 47 | $loader->load('asset.yml'); 48 | 49 | if ($config['override_default_package']) { 50 | $loader->load('fallback.yml'); 51 | 52 | $defaultPackage = $this->createPackage('default', [ 53 | 'prefix' => $config['prefix'], 54 | 'manifest' => $config['manifest'], 55 | ]); 56 | 57 | $defaultPackageId = $this->getPackageId('default'); 58 | $this->container->setDefinition($defaultPackageId, $defaultPackage); 59 | 60 | $this->container->getDefinition($this->namespaceService('package.fallback')) 61 | ->addArgument($config['fallback_patterns']) 62 | ->addArgument(new Reference($defaultPackageId)); 63 | } 64 | 65 | foreach ($config['packages'] as $name => $packageConfig) { 66 | $packageTag = $this->namespaceService('package.asset'); 67 | $package = $this->createPackage($name, $packageConfig) 68 | ->addTag($packageTag, ['alias' => $name]); 69 | 70 | $this->container->setDefinition($this->getPackageId($name), $package); 71 | } 72 | } 73 | 74 | /** 75 | * @param string $name 76 | * @param array $config 77 | * 78 | * @return Definition 79 | */ 80 | private function createPackage($name, array $config) 81 | { 82 | $prefixes = $config['prefix']; 83 | $isUrl = Util::containsUrl($prefixes); 84 | 85 | $packageDefinition = $isUrl 86 | ? new ChildDefinition($this->namespaceService('asset.package.url')) 87 | : new ChildDefinition($this->namespaceService('asset.package.path')) 88 | ; 89 | 90 | if ($config['manifest']['enabled']) { 91 | $versionStrategy = $this->createManifestVersionStrategy($name, $config['manifest']); 92 | } else { 93 | $versionStrategy = new Reference($this->namespaceService('version_strategy.empty')); 94 | } 95 | 96 | return $packageDefinition 97 | ->addArgument($isUrl ? $prefixes : $prefixes[0]) 98 | ->addArgument($versionStrategy) 99 | ->setPublic(false); 100 | } 101 | 102 | /** 103 | * @param string $name 104 | * 105 | * @return string 106 | */ 107 | private function getPackageId($name) 108 | { 109 | return $this->namespaceService("_package.$name"); 110 | } 111 | 112 | /** 113 | * @param string $packageName 114 | * @param array $config 115 | * 116 | * @return Reference 117 | */ 118 | private function createManifestVersionStrategy($packageName, $config) 119 | { 120 | $loader = new ChildDefinition($this->namespaceService('manifest.loader.'.$config['format'])); 121 | $loader 122 | ->addArgument($config['path']) 123 | ->addArgument($config['root_key']) 124 | ; 125 | 126 | $loaderId = $this->namespaceService("_package.$packageName.manifest_loader"); 127 | $this->container->setDefinition($loaderId, $loader); 128 | 129 | $cachedLoader = new ChildDefinition($this->namespaceService('manifest.loader.cached')); 130 | $cachedLoader->addArgument(new Reference($loaderId)); 131 | 132 | $cachedLoaderId = $this->namespaceService("_package.$packageName.manifest_loader_cached"); 133 | $this->container->setDefinition($cachedLoaderId, $cachedLoader); 134 | 135 | $versionStrategy = new ChildDefinition($this->namespaceService('version_strategy.manifest')); 136 | $versionStrategy->addArgument(new Reference($cachedLoaderId)); 137 | 138 | $versionStrategyId = $this->namespaceService("_package.$packageName.version_strategy"); 139 | $this->container->setDefinition($versionStrategyId, $versionStrategy); 140 | 141 | return new Reference($versionStrategyId); 142 | } 143 | 144 | /** 145 | * @param string $id 146 | * 147 | * @return string 148 | */ 149 | private function namespaceService($id) 150 | { 151 | return $this->alias.'.'.$id; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /DependencyInjection/Compiler/AssetCompilerPass.php: -------------------------------------------------------------------------------- 1 | getRegisteredPackages($container); 19 | 20 | foreach ($this->getTaggedPackages($container) as $id => $tags) { 21 | if (empty($tags) || !isset($tags[0]['alias'])) { 22 | throw new \LogicException( 23 | "The tag for the service with id '$id' must define an 'alias' attribute" 24 | ); 25 | } 26 | 27 | $packageName = $tags[0]['alias']; 28 | 29 | if (isset($registeredPackages[$packageName])) { 30 | throw new \LogicException( 31 | "A package named '$packageName' has already been registered" 32 | ); 33 | } 34 | 35 | if (isset($packages[$packageName])) { 36 | throw new \LogicException( 37 | "Multiple packages were found with alias '$packageName'. Package alias' must be unique" 38 | ); 39 | } 40 | 41 | $packages[$packageName] = $id; 42 | } 43 | 44 | $this->addPackages($packages, $container); 45 | 46 | if ($container->hasDefinition($this->namespaceService('package.fallback'))) { 47 | $this->setDefaultPackage($container); 48 | } 49 | } 50 | 51 | /** 52 | * @param ContainerBuilder $container 53 | * 54 | * @return Definition[] 55 | */ 56 | private function getTaggedPackages(ContainerBuilder $container) 57 | { 58 | return $container->findTaggedServiceIds($this->namespaceService('package.asset')); 59 | } 60 | 61 | /** 62 | * @param ContainerBuilder $container 63 | * 64 | * @return Definition 65 | */ 66 | private function getPackagesService(ContainerBuilder $container) 67 | { 68 | if (!$container->hasDefinition('assets.packages')) { 69 | throw new \LogicException('The Asset component is not registered in the container'); 70 | } 71 | 72 | return $container->getDefinition('assets.packages'); 73 | } 74 | 75 | /** 76 | * @param Definition[] $packages 77 | * @param ContainerBuilder $container 78 | */ 79 | private function addPackages($packages, ContainerBuilder $container) 80 | { 81 | $packagesService = $this->getPackagesService($container); 82 | 83 | foreach ($packages as $name => $id) { 84 | $packagesService->addMethodCall( 85 | 'addPackage', 86 | [$name, new Reference($id)] 87 | ); 88 | } 89 | } 90 | 91 | /** 92 | * @param ContainerBuilder $container 93 | */ 94 | private function setDefaultPackage(ContainerBuilder $container) 95 | { 96 | $packagesService = $this->getPackagesService($container); 97 | $defaultPackage = $this->getRegisteredDefaultPackage($container); 98 | $fallbackPackageId = $this->namespaceService('package.fallback'); 99 | 100 | $container->getDefinition($fallbackPackageId)->addMethodCall('setFallback', [$defaultPackage]); 101 | 102 | $packagesService->replaceArgument(0, new Reference($fallbackPackageId)); 103 | } 104 | 105 | /** 106 | * Retrieve packages that have already been registered. 107 | * 108 | * @param ContainerBuilder $container 109 | * 110 | * @return array with the packages' name as keys 111 | */ 112 | private function getRegisteredPackages(ContainerBuilder $container) 113 | { 114 | $arguments = $this->getPackagesService($container)->getArguments(); 115 | 116 | if (!isset($arguments[1]) || count($arguments[1]) < 2) { 117 | return []; 118 | } 119 | 120 | $argPackages = $arguments[1]; 121 | 122 | $packages = []; 123 | $argCount = count($argPackages); 124 | for ($i = 0; $i < $argCount; ++$i) { 125 | $packages[$argPackages[$i]] = $argPackages[++$i]; 126 | } 127 | 128 | return $packages; 129 | } 130 | 131 | /** 132 | * @param ContainerBuilder $container 133 | * 134 | * @return Definition|null 135 | */ 136 | private function getRegisteredDefaultPackage(ContainerBuilder $container) 137 | { 138 | $arguments = $this->getPackagesService($container)->getArguments(); 139 | 140 | if (!isset($arguments[0])) { 141 | return null; 142 | } 143 | 144 | return $arguments[0]; 145 | } 146 | 147 | /** 148 | * @param string $id 149 | * 150 | * @return string 151 | */ 152 | private function namespaceService($id) 153 | { 154 | return "rj_frontend.$id"; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | kernelRootDir = $kernelRootDir; 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function getConfigTreeBuilder() 32 | { 33 | $self = $this; 34 | 35 | return $this->createRoot('rj_frontend', 'array') 36 | ->children() 37 | ->booleanNode('override_default_package')->defaultTrue()->end() 38 | ->arrayNode('fallback_patterns') 39 | ->prototype('scalar')->end() 40 | ->defaultValue(['.*bundles\/.*']) 41 | ->end() 42 | ->append($this->addLivereloadSection()) 43 | ->append($this->addPackagePrefixSection(self::DEFAULT_PREFIX)) 44 | ->append($this->addPackageManifestSection()) 45 | ->arrayNode('packages') 46 | ->useAttributeAsKey('name') 47 | ->prototype('array') 48 | ->children() 49 | ->append($this->addPackagePrefixSection()) 50 | ->append($this->addPackageManifestSection()) 51 | ->end() 52 | ->beforeNormalization() 53 | ->ifTrue(function ($config) use ($self) { 54 | return $self->mustApplyManifestDefaultPath($config); 55 | }) 56 | ->then(function ($config) use ($self) { 57 | return $self->applyManifestDefaultPath($config); 58 | }) 59 | ->end() 60 | ->end() 61 | ->validate() 62 | ->ifTrue(function ($config) { 63 | return in_array('default', array_keys($config)); 64 | }) 65 | ->thenInvalid("'default' is a reserved package name") 66 | ->end() 67 | ->end() 68 | ->end() 69 | ->beforeNormalization() 70 | ->ifTrue(function ($config) use ($self) { 71 | return $self->mustApplyManifestDefaultPath($config); 72 | }) 73 | ->then(function ($config) use ($self) { 74 | return $self->applyManifestDefaultPath($config); 75 | }) 76 | ->end() 77 | ->end(); 78 | } 79 | 80 | private function addLivereloadSection() 81 | { 82 | return $this->createRoot('livereload') 83 | ->canBeDisabled() 84 | ->children() 85 | ->scalarNode('url') 86 | ->defaultValue('//localhost:35729/livereload.js') 87 | ->end() 88 | ->end() 89 | ; 90 | } 91 | 92 | private function addPackagePrefixSection($defaultValue = null) 93 | { 94 | $node = $this->createRoot('prefix') 95 | ->prototype('scalar')->end() 96 | ->defaultValue([$defaultValue]) 97 | ->requiresAtLeastOneElement() 98 | ->beforeNormalization() 99 | ->ifString() 100 | ->then(function ($v) { return [$v]; }) 101 | ->end() 102 | ->validate() 103 | ->ifTrue(function ($prefixes) { 104 | return Util::containsUrl($prefixes) 105 | && Util::containsNotUrl($prefixes); 106 | }) 107 | ->thenInvalid('Packages cannot have both URL and path prefixes') 108 | ->end() 109 | ->validate() 110 | ->ifTrue(function ($prefixes) { 111 | return count($prefixes) > 1 112 | && Util::containsNotUrl($prefixes); 113 | }) 114 | ->thenInvalid('Packages can only have one path prefix') 115 | ->end() 116 | ; 117 | 118 | return $defaultValue === null 119 | ? $node->isRequired() 120 | : $node 121 | ; 122 | } 123 | 124 | private function addPackageManifestSection() 125 | { 126 | return $this->createRoot('manifest') 127 | ->canBeEnabled() 128 | ->children() 129 | ->scalarNode('format') 130 | ->defaultValue('json') 131 | ->validate() 132 | ->ifNotInArray(['json']) 133 | ->thenInvalid('For the moment only JSON manifest files are supported') 134 | ->end() 135 | ->end() 136 | ->scalarNode('path')->isRequired()->end() 137 | ->scalarNode('root_key')->defaultNull()->end() 138 | ->end() 139 | ->beforeNormalization() 140 | ->ifString() 141 | ->then(function ($v) { return ['enabled' => true, 'path' => $v]; }) 142 | ->end() 143 | ; 144 | } 145 | 146 | /** 147 | * Returns true if the manifest's path has not been defined AND: 148 | * - a prefix has not been defined 149 | * - OR if a prefix has been defined, it's not a URL. 150 | * 151 | * Note that the manifest's configuration can be a string, in which case it 152 | * represents the path to the manifest file. 153 | * 154 | * This method is public because of the inability to use $this in closures 155 | * in PHP 5.3. 156 | * 157 | * @param array $config 158 | * 159 | * @return bool 160 | */ 161 | public function mustApplyManifestDefaultPath(array $config) 162 | { 163 | return isset($config['manifest']) && 164 | !is_string($config['manifest']) && 165 | !isset($config['manifest']['path']) && 166 | (!isset($config['prefix']) || !Util::containsUrl($config['prefix'])) 167 | ; 168 | } 169 | 170 | /** 171 | * Apply a default manifest path computed from the defined prefix. 172 | * 173 | * After calling this method, the manifest's path will be 174 | * %kernel.root_dir%/../web/$prefix/manifest.json, where $prefix is the 175 | * configured prefix. 176 | * 177 | * Note that this method is used for both the default package's config and 178 | * for each custom package's config. 179 | * 180 | * This method is public because of the inability to use $this in closures 181 | * in PHP 5.3 182 | * 183 | * @param array $config 184 | * 185 | * @return array 186 | */ 187 | public function applyManifestDefaultPath(array $config) 188 | { 189 | $prefix = isset($config['prefix']) ? $config['prefix'] : self::DEFAULT_PREFIX; 190 | 191 | if (is_array($prefix)) { 192 | $prefix = $prefix[0]; 193 | } 194 | 195 | if (!is_array($config['manifest'])) { 196 | $config['manifest'] = ['enabled' => true]; 197 | } 198 | 199 | $config['manifest']['path'] = implode('/', [ 200 | $this->kernelRootDir, 201 | '..', 202 | 'web', 203 | $prefix, 204 | 'manifest.json', 205 | ]); 206 | 207 | return $config; 208 | } 209 | 210 | /** 211 | * @param string $root 212 | * @param string|null $type 213 | * 214 | * @return ArrayNodeDefinition|NodeDefinition 215 | */ 216 | private function createRoot($root, $type = null) 217 | { 218 | $treeBuilder = new TreeBuilder(); 219 | 220 | if ($type !== null) { 221 | return $treeBuilder->root($root, $type); 222 | } 223 | 224 | return $treeBuilder->root($root); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /DependencyInjection/RjFrontendExtension.php: -------------------------------------------------------------------------------- 1 | getConfiguration([], $container); 19 | $config = $this->processConfiguration($configuration, $configs); 20 | 21 | $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config/')); 22 | $loader->load('console.yml'); 23 | $loader->load('version_strategy.yml'); 24 | $loader->load('manifest.yml'); 25 | 26 | if (version_compare(Kernel::VERSION, '3.3.0', '>=')) { 27 | $loader->load('commands.yml'); 28 | } 29 | 30 | if ($config['livereload']['enabled']) { 31 | $loader->load('livereload.yml'); 32 | $container->getDefinition($this->namespaceService('livereload.listener')) 33 | ->addArgument($config['livereload']['url']); 34 | } 35 | 36 | $assetExtensionLoader = new AssetExtensionLoader($this->getAlias(), $container); 37 | $assetExtensionLoader->load($config, $loader); 38 | } 39 | 40 | /** 41 | * @param array $config 42 | * @param ContainerBuilder $container 43 | * 44 | * @return Configuration 45 | */ 46 | public function getConfiguration(array $config, ContainerBuilder $container) 47 | { 48 | return new Configuration($container->getParameter('kernel.root_dir')); 49 | } 50 | 51 | /** 52 | * @param string $id 53 | * 54 | * @return string 55 | */ 56 | private function namespaceService($id) 57 | { 58 | return $this->getAlias().'.'.$id; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /EventListener/InjectLiveReloadListener.php: -------------------------------------------------------------------------------- 1 | url = $url; 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function onKernelResponse(FilterResponseEvent $event) 29 | { 30 | if (!$this->shouldInject($event)) { 31 | return; 32 | } 33 | 34 | $response = $event->getResponse(); 35 | $content = $response->getContent(); 36 | 37 | $pos = strripos($content, ''); 38 | if (false === $pos) { 39 | return; 40 | } 41 | 42 | $script = ''; 43 | $response->setContent(substr($content, 0, $pos).$script.substr($content, $pos)); 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public static function getSubscribedEvents() 50 | { 51 | return [ 52 | KernelEvents::RESPONSE => ['onKernelResponse', -128], 53 | ]; 54 | } 55 | 56 | /** 57 | * @param $event 58 | * 59 | * @return bool 60 | */ 61 | private function shouldInject(FilterResponseEvent $event) 62 | { 63 | if ($event->getRequestType() !== HttpKernelInterface::MASTER_REQUEST) { 64 | return false; 65 | } 66 | 67 | if ($event->getRequest()->isXmlHttpRequest()) { 68 | return false; 69 | } 70 | 71 | return $event->getResponse()->headers->has('X-Debug-Token'); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Paulo Rodrigues Pinto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Manifest/Loader/AbstractManifestLoader.php: -------------------------------------------------------------------------------- 1 | path = $path; 37 | $this->rootKey = $rootKey; 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function load() 44 | { 45 | $entries = $this->parse($this->path); 46 | 47 | if (!empty($this->rootKey)) { 48 | if (!isset($entries[$this->rootKey])) { 49 | throw new \InvalidArgumentException('Manifest file contains no '.$this->rootKey.' key'); 50 | } 51 | 52 | $entries = $entries[$this->rootKey]; 53 | } 54 | 55 | return new Manifest($entries); 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function getPath() 62 | { 63 | return $this->path; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Manifest/Loader/CachedManifestLoader.php: -------------------------------------------------------------------------------- 1 | getPath()).'.php.cache'; 29 | 30 | $this->cache = new ConfigCache($cachePath, $debug); 31 | $this->loader = $loader; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function load() 38 | { 39 | if (!$this->cache->isFresh()) { 40 | $resource = new FileResource($this->getPath()); 41 | $entries = $this->loader->load()->all(); 42 | 43 | $this->cache->write(sprintf('cache->getPath(); 46 | } 47 | 48 | return new Manifest($entries); 49 | } 50 | 51 | /** 52 | * {@inheritdoc} 53 | */ 54 | public function getPath() 55 | { 56 | $this->loader->getPath(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Manifest/Loader/JsonManifestLoader.php: -------------------------------------------------------------------------------- 1 | entries = $entries; 18 | } 19 | 20 | /** 21 | * @return array 22 | */ 23 | public function all() 24 | { 25 | return $this->entries; 26 | } 27 | 28 | /** 29 | * @param string $path 30 | * 31 | * @return bool 32 | */ 33 | public function has($path) 34 | { 35 | return array_key_exists($path, $this->entries); 36 | } 37 | 38 | /** 39 | * @param string $path 40 | * 41 | * @return null|string 42 | */ 43 | public function get($path) 44 | { 45 | if (!$this->has($path)) { 46 | return null; 47 | } 48 | 49 | return $this->entries[$path]; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Package/FallbackPackage.php: -------------------------------------------------------------------------------- 1 | patterns = $patterns; 31 | $this->package = $package; 32 | } 33 | 34 | /** 35 | * @param PackageInterface $fallback 36 | * 37 | * @return $this 38 | */ 39 | public function setFallback(PackageInterface $fallback) 40 | { 41 | $this->fallback = $fallback; 42 | 43 | return $this; 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function getVersion($path) 50 | { 51 | if ($this->mustFallback($path)) { 52 | return $this->fallback->getVersion($path); 53 | } 54 | 55 | return $this->package->getVersion($path); 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function getUrl($path, $version = null) 62 | { 63 | if ($this->mustFallback($path)) { 64 | return $this->fallback->getUrl($path); 65 | } 66 | 67 | return $this->package->getUrl($path); 68 | } 69 | 70 | /** 71 | * @param string $path 72 | * 73 | * @return bool 74 | */ 75 | protected function mustFallback($path) 76 | { 77 | foreach ($this->patterns as $pattern) { 78 | if (1 === preg_match("/$pattern/", $path)) { 79 | return true; 80 | } 81 | } 82 | 83 | return false; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # frontend-bundle 2 | A modern frontend development workflow for Symfony apps. 3 | 4 | # DEPRECATED 5 | This project is deprecated. Use [Webpack Encore](https://symfony.com/doc/current/frontend.html) instead. 6 | 7 | > Webpack Encore is a simpler way to integrate Webpack into your Symfony application. It wraps Webpack, giving you a clean & powerful API for bundling JavaScript modules, pre-processing CSS & JS and compiling and minifying assets. 8 | 9 | There used to be a time where Symfony had no first-class support for a modern frontend development workflow, which was the problem this bundle was trying to solve. With Webpack Encore that is no longer the case so there is no reason for this bundle to continue to exist. Webpack Encore does it better and is the officially recommended way to integrate frontend technologies into Symfony applications. 10 | 11 | --- 12 | 13 | [![Build Status](https://img.shields.io/travis/regularjack/frontend-bundle/master.svg?style=flat-square)](https://travis-ci.org/regularjack/frontend-bundle) 14 | [![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/regularjack/frontend-bundle.svg?style=flat-square)](https://scrutinizer-ci.com/g/regularjack/frontend-bundle/code-structure) 15 | [![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/regularjack/frontend-bundle.svg?style=flat-square)](https://scrutinizer-ci.com/g/regularjack/frontend-bundle) 16 | [![SensioLabsInsight](https://insight.sensiolabs.com/projects/5f7d6dc7-1dcb-4acf-86b7-eb1564c59939/mini.png)](https://insight.sensiolabs.com/projects/5f7d6dc7-1dcb-4acf-86b7-eb1564c59939) 17 | [![Packagist Version](https://img.shields.io/packagist/v/regularjack/frontend-bundle.svg?style=flat-square)](https://packagist.org/packages/regularjack/frontend-bundle) 18 | [![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](Resources/meta/LICENSE) 19 | [![Total Downloads](https://img.shields.io/packagist/dt/regularjack/frontend-bundle.svg?style=flat-square)](https://packagist.org/packages/regularjack/frontend-bundle) 20 | 21 | Symfony comes packaged with [Assetic](https://github.com/symfony/AsseticBundle) for managing frontend assets like CSS, JavaScript or images. Assetic is great to quickly start a project but, as applications grow, its limitations start to show. 22 | 23 | It has thus become more and more common to integrate tools native to frontend development into Symfony projects (`bower`, `gulp`, `webpack`, `livereload`, etc). However, setting up a seamless frontend development workflow is not easy and developers must repeat themselves every time they start a new project. 24 | 25 | [There](https://github.com/romanschejbal/gassetic) [are](https://github.com/Spea/SpBowerBundle) [several](https://github.com/francoispluchino/composer-asset-plugin) [tools](https://github.com/Kunstmaan/KunstmaanLiveReloadBundle) out there that make it easier to do this but they come with their own limitations and many are wrappers for the native frontend development tools. Developers should be able to use the native tools directly and have them just work within their Symfony projects. 26 | 27 | This bundle attempts to be the go-to solution for quickly, easily and cleanly setting up a tailored frontend development workflow in Symfony projects. 28 | 29 | *Supports PHP 5.4+, Symfony 2.7+* 30 | 31 | # Features 32 | * **Asset pipeline** 33 | * Automatically generate the build file for your preferred asset pipeline 34 | * Supports [Gulp](https://github.com/gulpjs/gulp) ([Webpack](https://webpack.github.io/), [Broccoli](https://github.com/broccolijs/broccoli) and others on the way) 35 | * Sensible defaults that work with most Symfony projects 36 | * You can easily adapt it for your use case 37 | * **Use Symfony's native calls to reference assets** 38 | * `` 39 | * No need to clutter your Twig templates with *boundaries* for the asset pipeline 40 | * Assets are automatically *cache-busted* in production 41 | * **Fast development** 42 | * Fast rebuilds make for an efficient workflow 43 | * Only changed files are processed 44 | * No more slow refreshes due to Assetic 45 | * **Livereload** 46 | * Browser updates when you save a file 47 | * Change the CSS, the browser instantaneously updates, without a page reload 48 | * **Bower** 49 | * Frontend dependencies are a `bower install` away 50 | * No more vendor code in your repository 51 | * Automatically generates `vendor.js` and `vendor.css` files from your `bower.json` 52 | * **Cache busting** 53 | * Automatically add a version to assets when in production 54 | * No more need to set a version on every deploy 55 | * An asset's version only changes if its content changed 56 | 57 | # Documentation 58 | [View Documentation](http://frontend-bundle.readthedocs.io) 59 | 60 | # Contributing 61 | [CONTRIBUTING](CONTRIBUTING.md) 62 | -------------------------------------------------------------------------------- /Resources/blueprints/bower.json.php: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "dependencies": { 4 | }, 5 | "overrides": { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Resources/blueprints/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psrpinto/frontend-bundle/083886026dd599c7c64a78cd58f6535edf46add4/Resources/blueprints/images/.keep -------------------------------------------------------------------------------- /Resources/blueprints/pipelines/gulp/gulpfile.js.php: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var del = require('del'); 3 | var gulp = require('gulp'); 4 | var util = require('gulp-util'); 5 | var cached = require('gulp-cached'); 6 | var concat = require('gulp-concat'); 7 | var filter = require('gulp-filter'); 8 | var add = require('gulp-add-src'); 9 | var flatten = require('gulp-flatten'); 10 | var replace = require('gulp-replace'); 11 | var babel = require('gulp-babel'); 12 | var uglify = require('gulp-uglify'); 13 | var minify = require('gulp-clean-css'); 14 | var livereload = require('gulp-livereload'); 15 | var revision = require('gulp-rev-all'); 16 | var env = require('gulp-environments'); 17 | 18 | var sass = require('gulp-sass'); 19 | 20 | var less = require('gulp-less'); 21 | 22 | 23 | var coffee = require('gulp-coffee'); 24 | 25 | 26 | var production = env.production; 27 | var development = env.development; 28 | 29 | var config = { 30 | // Full path to the directory containing the source assets. 31 | srcDir: path.join(__dirname, ), 32 | 33 | // Full path to the directory where compiled assets will be placed. 34 | // Must be a directory under `web/`. 35 | buildDir: path.join(__dirname, ), 36 | 37 | // Prepend references between assets with a prefix. 38 | // Will only be used in production builds. 39 | // urlPrefix: '//cdn.example.com', 40 | 41 | // Patterns for each kind of asset. 42 | // Relative to `config.srcDir`. 43 | sources: { 44 | images: 'images/**/*', 45 | stylesheets: ['stylesheets/**/*.', '!stylesheets/_*', '!stylesheets/vendor.'], 46 | scripts: 'scripts/**/*.coffeejs' 47 | }, 48 | 49 | // Sub-directories where compiled assets will be placed. 50 | // Relative to `config.buildDir`. 51 | targets: { 52 | images: 'images', 53 | stylesheets: 'css', 54 | scripts: 'js', 55 | fonts: 'fonts' 56 | }, 57 | 58 | // Vendor paths 59 | vendor: { 60 | stylesheet: path.join('stylesheets', 'vendor.'), 61 | bowerFile: path.join(__dirname, 'bower.json'), 62 | bowerComponents: path.join(__dirname, 'bower_components') 63 | }, 64 | }; 65 | 66 | // So that patterns passed to `gulp.src` are relative to `config.srcDir` 67 | process.chdir(config.srcDir); 68 | 69 | // Running `gulp` with no arguments will run the `watch` task 70 | gulp.task('default', ['watch']); 71 | 72 | /** 73 | * Copy images to their target directory. 74 | */ 75 | gulp.task('images', function() { 76 | return gulp.src(config.sources.images) 77 | .pipe(cached('images')) 78 | .pipe(gulp.dest(path.join(config.buildDir, config.targets.images))) 79 | .pipe(livereload()); 80 | }); 81 | 82 | /** 83 | * Compile stylesheets and place them in their target directory. 84 | */ 85 | gulp.task('stylesheets', function() { 86 | return gulp.src(config.sources.stylesheets) 87 | .pipe(cached('stylesheets')) 88 | 89 | .pipe(sass()) 90 | 91 | .pipe(less()) 92 | 93 | .pipe(rewriteCssUrls()) 94 | .pipe(production(minify())) 95 | .pipe(gulp.dest(path.join(config.buildDir, config.targets.stylesheets))) 96 | .pipe(livereload()); 97 | }); 98 | 99 | /** 100 | * Compile scripts and place them in their target directory. 101 | */ 102 | gulp.task('scripts', function() { 103 | return gulp.src(config.sources.scripts) 104 | .pipe(cached('scripts')) 105 | 106 | .pipe(coffee()) 107 | 108 | .pipe(babel()) 109 | .pipe(production(uglify())) 110 | .pipe(gulp.dest(path.join(config.buildDir, config.targets.scripts))) 111 | .pipe(livereload()); 112 | }); 113 | 114 | /** 115 | * All the vendor tasks. 116 | */ 117 | gulp.task('vendor', ['vendor:fonts', 'vendor:stylesheets', 'vendor:scripts']); 118 | 119 | /** 120 | * Copy font files required with Bower into their target directory. 121 | */ 122 | gulp.task('vendor:fonts', function() { 123 | return gulp.src(config.vendor.bowerFile) 124 | .pipe(bower()) 125 | .pipe(filter(['**/fonts/*', '**/font/*'])) 126 | .pipe(flatten()) 127 | .pipe(gulp.dest(path.join(config.buildDir, config.targets.fonts))) 128 | .pipe(livereload()); 129 | }); 130 | 131 | /** 132 | * Concatenate stylesheets required with Bower into `vendor.css` 133 | * AND 134 | * Compile `config.vendor.stylesheet` and append it to `vendor.css`. 135 | */ 136 | gulp.task('vendor:stylesheets', function() { 137 | return gulp.src(config.vendor.bowerFile) 138 | .pipe(bower()) 139 | .pipe(filter('**/*.css')) 140 | .pipe(add(config.vendor.stylesheet)) 141 | 142 | .pipe(sass()) 143 | 144 | .pipe(less()) 145 | 146 | .pipe(rewriteCssUrls()) 147 | .pipe(concat('vendor.css')) 148 | .pipe(production(minify())) 149 | .pipe(gulp.dest(path.join(config.buildDir, config.targets.stylesheets))) 150 | .pipe(livereload()); 151 | }); 152 | 153 | /** 154 | * Concatenate scripts required with Bower into `vendor.js`. 155 | */ 156 | gulp.task('vendor:scripts', function() { 157 | return gulp.src(config.vendor.bowerFile) 158 | .pipe(bower()) 159 | .pipe(filter('**/*.js')) 160 | .pipe(concat('vendor.js')) 161 | .pipe(production(uglify())) 162 | .pipe(gulp.dest(path.join(config.buildDir, config.targets.scripts))) 163 | .pipe(livereload()); 164 | }); 165 | 166 | /** 167 | * Build all assets. 168 | * 169 | * In production, every file will be revisioned by appending the hash of its 170 | * content to the filename, e.g. `foo.png` becomes `foo.abc123.png`. References 171 | * in other files will be rewritten in order to use the revisioned name. 172 | */ 173 | gulp.task('build', ['clean', 'images', 'stylesheets', 'scripts', 'vendor'], function(cb) { 174 | if (development()) { 175 | return cb(); 176 | } 177 | 178 | var rev = new revision({ 179 | fileNameManifest: 'manifest.json', 180 | hashLength: 24, 181 | prefix: getUrlPrefix() 182 | }); 183 | 184 | var sourceContents = require('fs').readdirSync(config.buildDir).map(function(entry) { 185 | return path.join(config.buildDir, entry); 186 | }); 187 | 188 | return gulp.src(config.buildDir + '/**/*') 189 | // move all files and directories to `src/` 190 | .pipe(gulp.dest(path.join(config.buildDir, 'src'))) 191 | .on('end', function() { 192 | del.sync(sourceContents, {force: true}); 193 | }) 194 | // revision assets in `src/` and place them in `config.buildDir` 195 | .pipe(rev.revision()) 196 | .pipe(gulp.dest(config.buildDir)) 197 | .pipe(rev.manifestFile()) 198 | .pipe(gulp.dest(config.buildDir)) 199 | // remove `src/` 200 | .on('end', function() { 201 | del.sync(path.join(config.buildDir, 'src'), {force: true}); 202 | }); 203 | }); 204 | 205 | /** 206 | * Trigger the right task when files change. 207 | */ 208 | gulp.task('watch', ['build'], function() { 209 | var watch = function(globs, taskName) { 210 | gulp.watch(globs, [taskName]).on('change', function(event) { 211 | if (event.type === 'deleted') { 212 | delete cached.caches[taskName][event.path]; 213 | } 214 | }); 215 | }; 216 | 217 | livereload.listen(); 218 | 219 | watch(config.sources.images, 'images'); 220 | watch(config.sources.stylesheets, 'stylesheets'); 221 | watch(config.sources.scripts, 'scripts'); 222 | 223 | watch(config.vendor.bowerFile, 'vendor'); 224 | watch(config.vendor.bowerComponents + '/**/*.js', 'vendor:scripts'); 225 | watch([config.vendor.bowerComponents + '/**/*.css', config.vendor.stylesheet], 'vendor:stylesheets'); 226 | }); 227 | 228 | /** 229 | * Clean up the build directory 230 | */ 231 | gulp.task('clean', function(cb) { 232 | del.sync([path.join(config.buildDir, '*')], {force: true}); 233 | cb(); 234 | }); 235 | 236 | /** 237 | * Rewrite `url()` calls in CSS files. 238 | * 239 | * Given 'assets' as `prefix`, the following `url()` calls: 240 | * 241 | * url(images/foo.jpg); 242 | * url('images/foo.jpg'); 243 | * url("images/foo.jpg"); 244 | * url(/images/foo.jpg); 245 | * url('/images/foo.jpg'); 246 | * url("/images/foo.jpg"); 247 | * url('../images/foo.jpg'); 248 | * url('../../images/foo.jpg'); 249 | * 250 | * would be rewritten to: 251 | * - development: url(/assets/images/foo.jpg) 252 | * - production: url(/images/foo.jpg) 253 | * 254 | * When in production, the prefix is not prepended because the `revision` step of 255 | * the build will do that. 256 | */ 257 | var rewriteCssUrls = function() { 258 | return replace( 259 | /url\(['"]?(?!data:)(\.\.\/)*\/?([^'"]+?)['"]?\)/g, 260 | development() ? 'url(' + getUrlPrefix() + '/$2)' : 'url(/$2)' 261 | ); 262 | }; 263 | 264 | /** 265 | * Retrieve the prefix to prepend to URLs. 266 | * 267 | * In development, the prefix is the directory under `web/`, e.g. '/assets' if 268 | * `config.buildDir` is `/path/to/web/assets`. 269 | * 270 | * In production, if `config.urlPrefix` is defined, it will be used. Otherwise, 271 | * the same prefix as in development will be used. 272 | */ 273 | var getUrlPrefix = function() { 274 | if (production() && config.hasOwnProperty('urlPrefix') && config.urlPrefix) { 275 | return config.urlPrefix; 276 | } 277 | 278 | var prefix = config.buildDir.replace(path.join(__dirname, 'web'), ''); 279 | while (prefix.charAt(0) === path.sep) { 280 | prefix = prefix.substring(1); 281 | } 282 | 283 | return '/' + prefix; 284 | }; 285 | 286 | /** 287 | * Utility function that calls gulp-main-bower-files while filtering the error 288 | * it throws if the bower_components/ directory does not exist. 289 | */ 290 | var bower = function() { 291 | return require('gulp-main-bower-files')({ 292 | bowerJson: config.vendor.bowerFile, 293 | bowerDirectory: config.vendor.bowerComponents 294 | }, function(error) { 295 | if (error && !error.toString().match(/^Error: Bower components directory does not exist/)) { 296 | util.log(error); 297 | } 298 | }); 299 | }; 300 | -------------------------------------------------------------------------------- /Resources/blueprints/pipelines/gulp/package.json.php: -------------------------------------------------------------------------------- 1 | { 2 | "main": "gulpfile.js", 3 | "description": "", 4 | "repository": "", 5 | "license": "MIT", 6 | "devDependencies": { 7 | "babel-core": "^6.26.0", 8 | "del": "^3.0.0", 9 | "gulp": "^3.9.0", 10 | "gulp-add-src": "^0.2.0", 11 | "gulp-babel": "^7.0.0", 12 | "gulp-cached": "^1.1.0", 13 | "gulp-clean-css": "^3.9.0", 14 | 15 | "gulp-coffee": "^2.3.1", 16 | 17 | "gulp-concat": "^2.6.0", 18 | "gulp-debug": "^3.1.0", 19 | "gulp-environments": "^0.1.1", 20 | "gulp-filter": "^5.0.1", 21 | "gulp-flatten": "^0.3.1", 22 | 23 | "gulp-less": "^3.3.2", 24 | 25 | "gulp-livereload": "^3.8.0", 26 | "gulp-main-bower-files": "^1.6.2", 27 | "gulp-replace": "^0.6.1", 28 | "gulp-rev-all": "^0.9.7", 29 | 30 | "gulp-sass": "^3.1.0", 31 | 32 | "gulp-uglify": "^3.0.0", 33 | "gulp-util": "^3.0.8" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Resources/blueprints/scripts/coffee/app.coffee: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psrpinto/frontend-bundle/083886026dd599c7c64a78cd58f6535edf46add4/Resources/blueprints/scripts/coffee/app.coffee -------------------------------------------------------------------------------- /Resources/blueprints/scripts/js/app.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psrpinto/frontend-bundle/083886026dd599c7c64a78cd58f6535edf46add4/Resources/blueprints/scripts/js/app.js -------------------------------------------------------------------------------- /Resources/blueprints/stylesheets/less/app.less.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psrpinto/frontend-bundle/083886026dd599c7c64a78cd58f6535edf46add4/Resources/blueprints/stylesheets/less/app.less.php -------------------------------------------------------------------------------- /Resources/blueprints/stylesheets/less/vendor.less.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psrpinto/frontend-bundle/083886026dd599c7c64a78cd58f6535edf46add4/Resources/blueprints/stylesheets/less/vendor.less.php -------------------------------------------------------------------------------- /Resources/blueprints/stylesheets/none/app.css.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psrpinto/frontend-bundle/083886026dd599c7c64a78cd58f6535edf46add4/Resources/blueprints/stylesheets/none/app.css.php -------------------------------------------------------------------------------- /Resources/blueprints/stylesheets/sass/app.scss.php: -------------------------------------------------------------------------------- 1 | // 2 | // This file is compiled to `//css/app.css`. 3 | // 4 | // Referencing other assets 5 | // ======================== 6 | // 7 | // To reference other assets like images or fonts, refer to them using the full 8 | // path. For example, to reference `images/foo/bar.jpg`: 9 | // 10 | // background-image: url(/images/foo/bar.jpg); 11 | // 12 | // // The leading slash is optional, the following also works: 13 | // // background-image: url(images/foo/bar.jpg); 14 | // 15 | // The compiled CSS will be: 16 | // 17 | // background-image: url(//images/foo/bar.jpg); 18 | // 19 | // There's no need to use relative paths, they make the code harder to reason about 20 | // and are unnecessary: 21 | // 22 | // // Don't do this! 23 | // background-image: url(../../images/foo/bar.jpg); 24 | // 25 | // Partials 26 | // ======== 27 | // 28 | // Partial Sass files contain snippets of reusable CSS that you can include in 29 | // other Sass files. A partial is simply a Sass file named with a leading 30 | // underscore, for example `_foo.scss`. 31 | // 32 | // Since partials are meant to be imported by other Sass files, they are not 33 | // compiled on their own so they won't appear under `/`. 34 | // 35 | // To import the file `./partials/_foo.scss`, you would do: 36 | // 37 | // @import "partials/foo"; 38 | // 39 | -------------------------------------------------------------------------------- /Resources/blueprints/stylesheets/sass/vendor.scss.php: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | // This file is compiled to `//css/vendor.css`. 4 | // 5 | // $fa-font-path: "/fonts"; 6 | // @import "../../../bower_components/font-awesome/scss/font-awesome"; 7 | // 8 | // `//css/vendor.css` will also contain assets imported with bower. 9 | // 10 | -------------------------------------------------------------------------------- /Resources/config/asset.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rj_frontend.asset.package.path: 3 | class: Symfony\Component\Asset\PathPackage 4 | public: false 5 | 6 | rj_frontend.asset.package.url: 7 | class: Symfony\Component\Asset\UrlPackage 8 | public: false 9 | -------------------------------------------------------------------------------- /Resources/config/commands.yml: -------------------------------------------------------------------------------- 1 | services: 2 | Rj\FrontendBundle\Command\: 3 | resource: '../../Command/*Command.php' 4 | tags: 5 | - { name: console.command } 6 | -------------------------------------------------------------------------------- /Resources/config/console.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rj_frontend.console.helper.question_legacy: 3 | class: Rj\FrontendBundle\Command\Options\Legacy\QuestionHelper 4 | -------------------------------------------------------------------------------- /Resources/config/fallback.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rj_frontend.package.fallback: 3 | class: Rj\FrontendBundle\Package\FallbackPackage 4 | public: false 5 | -------------------------------------------------------------------------------- /Resources/config/livereload.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rj_frontend.livereload.listener: 3 | class: Rj\FrontendBundle\EventListener\InjectLiveReloadListener 4 | tags: 5 | - { name: kernel.event_subscriber } 6 | -------------------------------------------------------------------------------- /Resources/config/manifest.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rj_frontend.manifest.loader.json: 3 | class: Rj\FrontendBundle\Manifest\Loader\JsonManifestLoader 4 | public: false 5 | 6 | rj_frontend.manifest.loader.cached: 7 | class: Rj\FrontendBundle\Manifest\Loader\CachedManifestLoader 8 | public: false 9 | arguments: 10 | - "%kernel.cache_dir%/rj_frontend" 11 | - "%kernel.debug%" 12 | -------------------------------------------------------------------------------- /Resources/config/version_strategy.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rj_frontend.version_strategy.empty: 3 | class: Rj\FrontendBundle\VersionStrategy\EmptyVersionStrategy 4 | public: false 5 | 6 | rj_frontend.version_strategy.manifest: 7 | class: Rj\FrontendBundle\VersionStrategy\ManifestVersionStrategy 8 | public: false 9 | -------------------------------------------------------------------------------- /Resources/doc/_static/custom.css: -------------------------------------------------------------------------------- 1 | .wy-menu-vertical p.caption { 2 | color: #CCCCCC; 3 | margin-top: 16px; 4 | margin-bottom: 0; 5 | margin-left: 3px; 6 | } 7 | .wy-menu-vertical p.caption .caption-text { 8 | font-size: 110%; 9 | } 10 | div.section #features li dl { 11 | margin-bottom: 0; 12 | } -------------------------------------------------------------------------------- /Resources/doc/bower.rst: -------------------------------------------------------------------------------- 1 | Using Bower 2 | =========== 3 | 4 | .. note :: 5 | 6 | In this section it's assumed your compiled assets are under ``web/assets/``. 7 | 8 | `Bower `_ allows you to require frontend assets in a similar way to what Composer does for PHP packages. Instead of commiting third-party code to your repository, you simply add your dependencies to a ``bower.json`` file. 9 | 10 | For example, if you wanted to use `Bootstrap `_ you would do: 11 | 12 | .. code-block :: shell 13 | 14 | bower install --save bootstrap 15 | 16 | .. note :: 17 | 18 | The ``--save`` flag adds the dependency to ``bower.json`` 19 | 20 | Since Bootstrap requires jquery, Bower installed both under ``bower_components/``: 21 | 22 | .. code :: 23 | 24 | bower_components/ 25 | ├── bootstrap/ 26 | └── jquery/ 27 | 28 | Requiring JavaScript and CSS assets 29 | ----------------------------------- 30 | JavaScript and CSS assets installed with Bower are automatically compiled into ``web/assets/js/vendor.js`` and ``web/assets/css/vendor.css``, respectively, **along with their dependencies and in the correct order**. 31 | 32 | .. tip :: 33 | 34 | For information on how this works see :ref:`bower-main-files`. 35 | 36 | To reference the ``vendor.js`` file from your template: 37 | 38 | .. code-block :: html 39 | 40 | 41 | 42 | And to reference the ``vendor.css`` file: 43 | 44 | .. code-block :: html 45 | 46 | 47 | 48 | Requiring SASS or Less stylesheets 49 | ---------------------------------- 50 | SASS or Less stylesheets installed with bower, as opposed to CSS assets, are not automatically compiled into ``web/assets/css/vendor.css``. This is because they are meant to be used differently. 51 | 52 | Depending on your use case, when requiring SASS or Less stylesheets you typically want one of the following: 53 | 54 | 1. Compile the vendor stylesheet into ``vendor.css``, while overriding variables 55 | 2. Include the vendor stylesheet in your own stylesheets so that you can use mixins, ``@extend`` rules, etc 56 | 57 | Compiling into ``vendor.css`` 58 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 59 | The ``app/Resources/stylesheets/vendor.scss`` (or ``.less``) file will be compiled and appended to ``web/assets/css/vendor.css`` (after any potential CSS that is automatically included from your Bower dependencies). This is useful when you simply want to compile some vendor stylesheet but don't need to use its mixins in your own stylesheets. 60 | 61 | As an example, consider you wanted to use `Font Awesome `_: 62 | 63 | .. code-block :: shell 64 | 65 | bower install --save font-awesome 66 | 67 | Then you need to specify that you're interested in its font assets: 68 | 69 | .. code :: 70 | 71 | // bower.json 72 | 73 | { 74 | "dependencies": { 75 | "font-awesome": "~4.4.0" 76 | }, 77 | "overrides": { 78 | "font-awesome": { 79 | "main": "fonts/*" 80 | } 81 | } 82 | } 83 | 84 | .. tip :: 85 | 86 | For more information on why this is needed see :ref:`bower-overriding-a-packages-settings`. 87 | 88 | When using Font Awesome, you have to set the path to the fonts directory so that ``url()`` calls use the correct path. Since font assets required with Bower are automatically "compiled" into ``web/assets/fonts/``, you would simply do: 89 | 90 | .. code-block :: SCSS 91 | 92 | // app/Resources/stylesheets/vendor.scss 93 | 94 | // Use the absolute path to the fonts directory, relative to `web/assets`. 95 | // This ensures paths will be rewritten correctly when in production. 96 | $fa-font-path: "fonts"; 97 | 98 | @import "../../../bower_components/font-awesome/scss/font-awesome"; 99 | 100 | If you now build your assets and look into ``web/assets/css/vendor.css`` you'll see Font Awesome's code, where the ``url()`` calls are something like: 101 | 102 | .. code-block :: css 103 | 104 | url(/assets/fonts/fontawesome-webfont.eot) 105 | 106 | Or, in production: 107 | 108 | .. code-block :: css 109 | 110 | url(/assets/fonts/fontawesome-webfont.123abc.eot) 111 | 112 | Including in your own stylesheets 113 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 114 | Instead of compiling a vendor stylesheet into ``vendor.css``, it's sometimes better to ``@import`` it in your own stylesheet instead. This is the case when you want to use mixins or variables defined by the vendor stylesheet. 115 | 116 | Following Font Awesome's example above, suppose you wanted to use its mixins in your stylesheet: 117 | 118 | .. code-block :: SCSS 119 | 120 | // app/Resources/stylesheets/app.scss 121 | 122 | $fa-font-path: "fonts"; 123 | @import "../../../bower_components/font-awesome/scss/variables"; 124 | @import "../../../bower_components/font-awesome/scss/mixins"; 125 | 126 | .foo { 127 | @include fa-icon(); 128 | } 129 | 130 | .. note :: 131 | 132 | Note that this is just an example and not the correct usage of Font Awesome. In a real application you would never use the ``fa-icon`` mixin directly. 133 | 134 | .. _bower-overriding-a-packages-settings: 135 | 136 | Overriding a package's settings 137 | ------------------------------- 138 | 139 | .. _bower-main-files: 140 | 141 | Main Files 142 | ~~~~~~~~~~ 143 | We're able to automatically generate the ``vendor.css`` and ``vendor.js`` files from ``bower.json`` because Bower packages, in their own ``bower.json``, define their *main files*. 144 | 145 | For example, if you look into Bootstrap's ``bower.json``, you will see something like: 146 | 147 | .. code :: 148 | 149 | // bower_components/bootstrap/bower.json 150 | 151 | "main": [ 152 | "less/bootstrap.less", 153 | "dist/js/bootstrap.js" 154 | ], 155 | 156 | By parsing this file, we're able to automatically add the ``bootstrap.js`` file to our ``vendor.js``. 157 | 158 | However, as you can see above, Bootstrap does not define ``bootstrap.css`` as a main file. If you wanted to automatically include ``bootstrap.css`` into your ``vendor.css``, you would *override* the main files defined in Bootstrap's ``bower.json`` by adding the following to your own ``bower.json``: 159 | 160 | .. code :: 161 | 162 | // bower.json 163 | 164 | { 165 | "dependencies": { 166 | "bootstrap": "~3.3.5" 167 | }, 168 | "overrides": { 169 | "bootstrap": { 170 | "main": "dist/css/bootstrap.css" 171 | } 172 | } 173 | } 174 | 175 | If you then build your assets you'll see that ``bootstrap.css`` is present in your ``vendor.css``. 176 | 177 | Dependencies in the same package 178 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 179 | If you need to specify dependencies between Bower assets in a given package, you can do so in the ``overrides`` section of you ``bower.json``. 180 | 181 | For example, instead of including all of Bootstrap's JavaScript, suppose you only needed ``popover.js``. Since ``popover.js`` requires ``tooltip.js``, your ``bower.json`` would be: 182 | 183 | .. code :: 184 | 185 | // bower.json 186 | 187 | { 188 | "dependencies": { 189 | "bootstrap": "~3.3.5" 190 | }, 191 | "overrides": { 192 | "bootstrap": { 193 | "main": [ 194 | "js/tooltip.js", 195 | "js/popover.js" 196 | ] 197 | } 198 | } 199 | } 200 | 201 | You'll then find both files in your ``vendor.js``, in the order you specified. 202 | 203 | Dependencies between packages 204 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 205 | Sometimes you run into Bower packages that do not correctly specify dependencies in their ``bower.json``. 206 | 207 | As an example, suppose you wanted to use a ``foo`` package that requires ``jquery`` but does not specify that in their ``bower.json``. You would install both packages using Bower: 208 | 209 | .. code-block :: shell 210 | 211 | bower install --save foo jquery 212 | 213 | And then setup the dependency in your ``bower.json``: 214 | 215 | .. code :: 216 | 217 | // bower.json 218 | 219 | { 220 | "dependencies": { 221 | "foo": "0.0.1", 222 | "jquery": "~2.1.4" 223 | }, 224 | "overrides": { 225 | "foo": { 226 | "dependencies": { 227 | "jquery": "*" 228 | } 229 | } 230 | } 231 | } 232 | 233 | This ensures that, in your ``vendor.js``, ``jquery.js`` will appear before ``foo.js``. -------------------------------------------------------------------------------- /Resources/doc/conf.py: -------------------------------------------------------------------------------- 1 | # 2 | # Sphinx configuration file 3 | # 4 | # See CONTRIBUTING.md for instructions on how to build the documentation. 5 | # 6 | 7 | import sphinx_rtd_theme 8 | from sphinx.highlighting import lexers 9 | from pygments.lexers.web import PhpLexer 10 | 11 | project = u'frontend-bundle' 12 | 13 | extensions = [] 14 | nitpicky = True 15 | master_doc = 'index' 16 | suppress_warnings = ['image.nonlocal_uri'] 17 | 18 | html_show_copyright = False 19 | html_theme = 'sphinx_rtd_theme' 20 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 21 | html_static_path = ['_static'] 22 | 23 | # Allow omiting `` 23 | 24 | And have it output: 25 | 26 | .. code-block :: html 27 | 28 | 29 | 30 | The answer is: by using a *manifest* file that maps the original filename to the filename with the hash appended. When you run ``gulp build --env production`` you will also get a ``manifest.json`` file that looks something like: 31 | 32 | .. code-block :: json 33 | 34 | { 35 | "js/foo.js": "js/foo-123abc.js" 36 | } 37 | 38 | Using the manifest 39 | ~~~~~~~~~~~~~~~~~~ 40 | Using the manifest file to generate the URLs is disabled by default. You need to enable it for the production environment: 41 | 42 | .. code-block :: yaml 43 | 44 | # app/config/config_prod.yml 45 | 46 | rj_frontend: 47 | manifest: true 48 | 49 | Changing the manifest path 50 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 51 | By default, the manifest is expected to be found under ``web/assets/manifest.json``. If you need to change this, you would add the following to your ``app/config/config_prod.yml`` file: 52 | 53 | .. code-block :: yaml 54 | 55 | # app/config/config_prod.yml 56 | 57 | rj_frontend: 58 | manifest: "%kernel.root_dir%/../web/foo/manifest.json" 59 | 60 | Configuring assets to never expire 61 | ---------------------------------- 62 | Since an asset's filename will change if its content changes, you can safely tell browsers to cache all assets indefinitely. You can do that by having your webserver set the ``Expires`` header to a value in the far future, say one year. 63 | 64 | If you're using `nginx `_, you can do this by adding the following ``location`` block to the ``server`` configuration: 65 | 66 | .. code-block :: nginx 67 | 68 | location /assets/ { 69 | expires 1y; 70 | } 71 | 72 | If using `Apache `_, make sure you have `mod_expires `_ active and add the following to your configuration: 73 | 74 | .. code-block :: apache 75 | 76 | 77 | 78 | ExpiresActive on 79 | ExpiresDefault "access plus 1 year" 80 | 81 | 82 | 83 | Using a CDN 84 | ----------- 85 | When serving assets from a Content Delivery Network, you want to use an absolute URL, for example: 86 | 87 | .. code-block :: html 88 | 89 | 90 | 91 | You can do this with the following configuration: 92 | 93 | .. code-block :: yaml 94 | 95 | # app/config/config_prod.yml 96 | 97 | rj_frontend: 98 | prefix: //cdn.example.com/ 99 | manifest: true 100 | 101 | .. note :: 102 | 103 | The manifest file must still be present locally in your server 104 | 105 | You also want references between assets to use the absolute URL, like when referencing images from your stylesheets. In your ``gulpfile.js`` you can set an URL prefix to use in production as follows: 106 | 107 | .. code-block :: js 108 | 109 | // gulpfile.js 110 | 111 | var config = { 112 | ... 113 | // Prepend references between assets with a prefix. 114 | // Will only be used in production builds. 115 | urlPrefix: '//cdn.example.com', 116 | ... 117 | }; 118 | -------------------------------------------------------------------------------- /Resources/doc/directory-structure.rst: -------------------------------------------------------------------------------- 1 | Directory Structure 2 | =================== 3 | This section describes the default directory structure for both source and compiled assets. The default directory structure follows Symfony's best practices and conventions as much as possible, as long as they make sense for the use case. 4 | 5 | You're free to change this directory structure as you see fit but **we recommend you use the default** one. If you do change it, remember to update your ``gulpfile.js`` accordingly. 6 | 7 | Here's an example of the directory structure of the source assets and the corresponding compiled assets: 8 | 9 | .. code :: 10 | 11 | # Sources # Compiled 12 | 13 | app/Resources web/assets 14 | ├── images ├── images 15 | │   ├── foo.png │   ├── foo.png 16 | ├── scripts ├── js 17 | │   ├── app.coffee │   ├── app.js 18 | └── stylesheets └── css 19 | └── app.scss └── app.css 20 | 21 | Source Assets 22 | ------------- 23 | Symfony's best practices `recommend `_ you store your source assets under ``web/``, which means they will be publicly available. However, in our case, this doesn't make sense because those assets are meant to be compiled: you don't want your ``.scss`` or ``.coffee`` sources to be publicly available. 24 | 25 | Having assets under ``app/Resources/`` solves that problem and has the added advantage that they're right next to the templates, under ``app/Resources/views/``, which is the `best-practice location `_ for storing templates. 26 | 27 | Compiled Assets 28 | --------------- 29 | Compiled assets are publicly visible so they must be stored in a directory under ``web/``. By default, they're stored under ``web/assets``. 30 | 31 | To use a directory other than ``web/assets`` just modify your ``gulpfile.js`` accordingly: 32 | 33 | .. code-block :: js 34 | 35 | // gulpfile.js 36 | 37 | var config = { 38 | buildDir: path.join(__dirname, 'web/foo'), 39 | // .. 40 | }; 41 | 42 | You also need to make sure that your bundle configuration references the correct directory: 43 | 44 | .. code-block :: yaml 45 | 46 | # app/config/config.yml 47 | rj_frontend: 48 | prefix: foo 49 | -------------------------------------------------------------------------------- /Resources/doc/index.rst: -------------------------------------------------------------------------------- 1 | frontend-bundle 2 | =============== 3 | A modern frontend development workflow for Symfony apps 4 | 5 | DEPRECATED 6 | ---------- 7 | This project is deprecated. Use `Webpack Encore `_ instead. 8 | 9 | Webpack Encore is a simpler way to integrate Webpack into your Symfony application. It wraps Webpack, giving you a clean & powerful API for bundling JavaScript modules, pre-processing CSS & JS and compiling and minifying assets. 10 | 11 | There used to be a time where Symfony had no first-class support for a modern frontend development workflow, which was the problem this bundle was trying to solve. With Webpack Encore that is no longer the case so there is no reason for this bundle to continue to exist. Webpack Encore does it better and is the officially recommended way to integrate frontend technologies into Symfony applications. 12 | 13 | --------- 14 | 15 | .. image:: https://img.shields.io/travis/regularjack/frontend-bundle/master.svg?style=flat-square 16 | :alt: Build Status 17 | :target: https://travis-ci.org/regularjack/frontend-bundle 18 | .. image:: https://img.shields.io/scrutinizer/coverage/g/regularjack/frontend-bundle.svg?style=flat-square 19 | :alt: Coverage Status 20 | :target: https://scrutinizer-ci.com/g/regularjack/frontend-bundle/code-structure 21 | .. image:: https://img.shields.io/scrutinizer/g/regularjack/frontend-bundle.svg?style=flat-square 22 | :alt: Scrutinizer Code Quality 23 | :target: https://scrutinizer-ci.com/g/regularjack/frontend-bundle 24 | .. image:: https://insight.sensiolabs.com/projects/5f7d6dc7-1dcb-4acf-86b7-eb1564c59939/mini.png 25 | :alt: SensioLabsInsight 26 | :target: https://insight.sensiolabs.com/projects/5f7d6dc7-1dcb-4acf-86b7-eb1564c59939 27 | .. image:: https://img.shields.io/packagist/v/regularjack/frontend-bundle.svg?style=flat-square 28 | :alt: Packagist Version 29 | :target: https://packagist.org/packages/regularjack/frontend-bundle 30 | .. image:: https://img.shields.io/packagist/dt/regularjack/frontend-bundle.svg?style=flat-square 31 | :alt: Total Downloads 32 | :target: https://packagist.org/packages/regularjack/frontend-bundle 33 | 34 | Symfony comes packaged with `Assetic `_ for managing frontend assets like CSS, JavaScript or images. Assetic is great to quickly start a project but, as applications grow, its limitations start to show. 35 | 36 | It has thus become more and more common to integrate tools native to frontend development into Symfony projects (`bower`, `gulp`, `webpack`, `livereload`, etc). However, setting up a seamless frontend development workflow is not easy and developers must repeat themselves every time they start a new project. 37 | 38 | `There `_ `are `_ `several `_ `tools `_ out there that make it easier to do this but they come with their own limitations and many are wrappers for the native frontend development tools. Developers should be able to use the native tools directly and have them just work within their Symfony projects. 39 | 40 | This bundle attempts to be the go-to solution for quickly, easily and cleanly setting up a tailored frontend development workflow in Symfony projects. 41 | 42 | *Supports PHP 5.3+, Symfony 2.3+* 43 | 44 | Features 45 | -------- 46 | * **Asset pipeline** 47 | * Automatically generate the build file for your preferred asset pipeline 48 | * Supports `Gulp `_, (`Webpack `_, `Broccoli `_ and others on the way) 49 | * Sensible defaults that work with most Symfony projects 50 | * You can easily adapt it for your use case 51 | * **Use Symfony's native calls to reference assets** 52 | * ```` 53 | * No need to clutter your Twig templates with *boundaries* for the asset pipeline 54 | * Assets are automatically *cache-busted* in production 55 | * **Fast development** 56 | * Fast rebuilds make for an efficient workflow 57 | * Only changed files are processed 58 | * No more slow refreshes due to Assetic 59 | * **Livereload** 60 | * Browser updates when you save a file 61 | * Change the CSS, the browser instantaneously updates, without a page reload 62 | * **Bower** 63 | * Frontend dependencies are a ``bower install`` away 64 | * No more vendor code in your repository 65 | * Automatically generates ``vendor.js`` and ``vendor.css`` files from your ``bower.json`` 66 | * **Cache busting** 67 | * Automatically add a version to assets when in production 68 | * No more need to set a version on every deploy 69 | * An asset's version only changes if its content changed 70 | 71 | Table of Contents 72 | ----------------- 73 | .. toctree :: 74 | :maxdepth: 1 75 | 76 | setup 77 | directory-structure 78 | referencing-assets 79 | bower 80 | deployment 81 | 82 | License 83 | ------- 84 | MIT 85 | -------------------------------------------------------------------------------- /Resources/doc/referencing-assets.rst: -------------------------------------------------------------------------------- 1 | Referencing Assets 2 | ================== 3 | 4 | .. note :: 5 | 6 | In this section it's assumed your compiled assets are located under ``web/assets/``. 7 | 8 | In templates 9 | ------------ 10 | To reference an asset from a template, you do as you normally would, with Symfony's ``asset`` helper: 11 | 12 | .. code-block :: html 13 | 14 | 15 | 16 | .. note :: 17 | 18 | You're referencing the **compiled** asset, from the ``web/assets`` directory, not the source asset. 19 | 20 | This will automatically prefix, and when in production *cache-bust*, the URL so the previous call would ouput: 21 | 22 | .. code-block :: html 23 | 24 | 25 | 26 | Or, in production: 27 | 28 | .. code-block :: html 29 | 30 | 31 | 32 | In styleshets 33 | ------------- 34 | It's common that you need to reference images from your stylesheets. To do that, use the ``url()`` notation and the full path to the image, relative to ``web/assets/``: 35 | 36 | .. code-block :: css 37 | 38 | background-image: url(images/foo.png); 39 | 40 | .. note :: 41 | 42 | Remember that you're referencing the **compiled** asset, from the ``web/assets`` directory, not the source asset. 43 | 44 | .. tip :: 45 | 46 | Never reference images in stylesheets with a relative path like ``../images/foo.png``. Relative paths make the code harder to reason about, are unnecessary and will be converted to the absolute path (i.e. ``../`` is stripped). 47 | 48 | The compiled CSS would be: 49 | 50 | .. code-block :: css 51 | 52 | background-image: url(/assets/images/foo.png); 53 | 54 | Or, in production: 55 | 56 | .. code-block :: css 57 | 58 | background-image: url(/assets/images/foo-123abc.png); 59 | -------------------------------------------------------------------------------- /Resources/doc/setup.rst: -------------------------------------------------------------------------------- 1 | Setup 2 | ===== 3 | Installation 4 | ------------ 5 | Install with composer: 6 | 7 | .. code-block:: shell 8 | 9 | composer require regularjack/frontend-bundle 10 | 11 | Add to your ``AppKernel.php``: 12 | 13 | .. code-block:: php 14 | 15 | // app/AppKernel.php 16 | 17 | public function registerBundles() 18 | { 19 | $bundles = array( 20 | // ... 21 | new Rj\FrontendBundle\RjFrontendBundle(), 22 | ); 23 | } 24 | 25 | Node.js must be installed on your system. You can find installation instructions on `Node's website `_. 26 | 27 | Once Node is installed, run: 28 | 29 | .. code-block:: shell 30 | 31 | npm install -g bower 32 | npm install -g gulp-cli 33 | 34 | .. note :: 35 | 36 | Only gulp is supported at the moment, other asset pipelines are on the way. 37 | From now on, this document will assume you're using `gulp`. 38 | 39 | Configuration 40 | ------------- 41 | .. tip :: 42 | 43 | If you're starting a new project, no configuration is needed at this point and you can safely skip this step. 44 | 45 | Symfony has the notion of an *asset package* which allows you to group related assets together. For example, you could have an *app* package and an *admin* package which you reference as follows: 46 | 47 | .. code-block :: html 48 | 49 | 50 | 51 | 52 | When the second argument to the ``asset()`` Twig helper is omitted, you're in fact using the *default package*: 53 | 54 | .. code-block :: html 55 | 56 | 57 | 58 | This bundle *overrides* the default package in order for you to not to have to pass the second argument. If you're integrating this bundle into an existing application, which expects the ``asset()`` helper to behave as it usually does, you must disable this *magic* and explicitely specify a *package* to use: 59 | 60 | .. code-block :: yaml 61 | 62 | # app/config/config.yml 63 | 64 | rj_frontend: 65 | override_default_package: false 66 | packages: 67 | mypackage: 68 | prefix: assets 69 | 70 | This will ensure that existing ``asset()`` calls will keep functioning as expected and you can then progressively migrate to this bundle by using the ``mypackage`` package: 71 | 72 | .. code-block :: html 73 | 74 | 75 | 76 | Setting up the asset pipeline 77 | ----------------------------- 78 | A console command is provided that allows you to generate a ``gulpfile.js`` tailored for your project. The command will ask you a set of questions (Where are your source assets? Where should the compiled assets be placed? Which CSS pre-processor you wish to use? Etc.) and use your answers to generate the ``gulpfile.js``. 79 | 80 | After running the command you'll have a functioning ``gulpfile.js`` and the directory tree for your source assets under ``app/Resources/`` (or wherever you decided to place them). 81 | 82 | You can run the command with: 83 | 84 | .. code-block :: shell 85 | 86 | app/console rj_frontend:setup 87 | 88 | Or one of the following: 89 | 90 | .. code-block :: shell 91 | 92 | # Output which commands would have been run instead of running them 93 | app/console rj_frontend:setup --dry-run 94 | 95 | # Use default values for all the options 96 | app/console rj_frontend:setup --no-interaction 97 | 98 | # Use Less and CoffeeScript, ask for the other options 99 | app/console rj_frontend:setup --csspre=less --coffee=true 100 | 101 | # Use Less and CoffeeScript, use defaults for other options 102 | app/console rj_frontend:setup --csspre=less --coffee=true --no-interaction 103 | 104 | You can read about all available options with: 105 | 106 | .. code-block :: shell 107 | 108 | app/console rj_frontend:setup --help 109 | 110 | .. tip :: 111 | 112 | Feel free to take a look at the generated ``gulpfile.js``. Even though the file is somewhat long, it should be straightforward to understand so you'll be able to adapt it to your use case, if need be. 113 | 114 | Finally, install ``npm`` dependencies required by ``gulpfile.js``: 115 | 116 | .. code-block :: shell 117 | 118 | npm install 119 | 120 | Livereload 121 | ---------- 122 | 123 | .. note :: 124 | 125 | Livereload is enabled by default in development and disabled in production. 126 | 127 | With Livereload enabled, all the requests that return a response with a closing ``body`` tag will have the following injected into the HTML, right before ````: 128 | 129 | .. code-block :: html 130 | 131 | 132 | 133 | If, for some reason, you need to change the URL, you can do so with the following configuration: 134 | 135 | .. code-block :: yaml 136 | 137 | # app/config/config_dev.yml 138 | rj_frontend: 139 | livereload: 140 | url: //example.com:1234/livereload.js 141 | 142 | .. note :: 143 | 144 | The configuration should be added to ``app/config/config_dev.yml`` since it does not apply in other environments. 145 | 146 | If you wish to not have the livereload script injected, you can do so with the following configuration: 147 | 148 | .. code-block :: yaml 149 | 150 | # app/config/config_dev.yml 151 | rj_frontend: 152 | livereload: false 153 | 154 | Next steps 155 | ---------- 156 | You're done with setup! In development, simply run the following command, and leave it running. Assets will be recompiled when changed and livereload will be triggered: 157 | 158 | .. code-block :: shell 159 | 160 | gulp 161 | 162 | If you just want to build the assets but not watch for changes: 163 | 164 | .. code-block :: shell 165 | 166 | gulp build 167 | 168 | To build the assets for the production environment run: 169 | 170 | .. code-block :: shell 171 | 172 | gulp build --env production 173 | -------------------------------------------------------------------------------- /RjFrontendBundle.php: -------------------------------------------------------------------------------- 1 | addCompilerPass(new AssetCompilerPass()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/Command/SetupCommandTest.php: -------------------------------------------------------------------------------- 1 | remove($this->baseDir = sys_get_temp_dir().'/rj_frontend'); 31 | mkdir($this->baseDir); 32 | 33 | $application = new Application(); 34 | 35 | $application->add($command = new SetupCommand()); 36 | 37 | $this->command = $application->find($command->getName()); 38 | $this->command->setRootDir($this->baseDir); 39 | 40 | $this->commandTester = new CommandTester($this->command); 41 | } 42 | 43 | public function testDefaultOptions() 44 | { 45 | $this->assertOptions(['src-dir' => null], ['src-dir' => $this->baseDir.'/app/Resources']); 46 | $this->assertOptions(['dest-dir' => null], ['dest-dir' => $this->baseDir.'/web/assets']); 47 | $this->assertOptions(['pipeline' => null], ['pipeline' => 'gulp']); 48 | $this->assertOptions(['csspre' => null], ['csspre' => 'sass']); 49 | $this->assertOptions(['coffee' => null], ['coffee' => false]); 50 | } 51 | 52 | public function testOptions() 53 | { 54 | $this->assertOptions(['src-dir' => 'foo'], ['src-dir' => 'foo']); 55 | $this->assertOptions(['dest-dir' => 'web/foo'], ['dest-dir' => 'web/foo']); 56 | $this->assertOptions(['pipeline' => 'gulp'], ['pipeline' => 'gulp']); 57 | $this->assertOptions(['csspre' => 'less'], ['csspre' => 'less']); 58 | $this->assertOptions(['coffee' => true], ['coffee' => true]); 59 | } 60 | 61 | public function testOptionsNoInteraction() 62 | { 63 | $this->assertOptions([], [ 64 | 'src-dir' => $this->baseDir.'/app/Resources', 65 | 'dest-dir' => $this->baseDir.'/web/assets', 66 | 'pipeline' => 'gulp', 67 | 'csspre' => 'sass', 68 | 'coffee' => false, 69 | ], false); 70 | 71 | $this->assertOptions(['src-dir' => 'foo'], ['src-dir' => 'foo', 'dest-dir' => $this->baseDir.'/web/assets'], false); 72 | $this->assertOptions(['dest-dir' => 'web/foo'], ['src-dir' => $this->baseDir.'/app/Resources', 'dest-dir' => 'web/foo'], false); 73 | $this->assertOptions(['pipeline' => 'gulp'], ['src-dir' => $this->baseDir.'/app/Resources', 'pipeline' => 'gulp'], false); 74 | $this->assertOptions(['csspre' => 'less'], ['src-dir' => $this->baseDir.'/app/Resources', 'csspre' => 'less'], false); 75 | $this->assertOptions(['coffee' => 'true'], ['src-dir' => $this->baseDir.'/app/Resources', 'coffee' => true], false); 76 | } 77 | 78 | /** 79 | * @expectedException \InvalidArgumentException 80 | * @expectedExceptionMessage 'dest-dir' must be a directory under web/ 81 | */ 82 | public function testInvalidDestDir() 83 | { 84 | $this->commandTester->execute(['--dest-dir' => 'foo'], ['interactive' => false]); 85 | } 86 | 87 | /** 88 | * @expectedException \InvalidArgumentException 89 | * @expectedExceptionMessage 'dest-dir' must be a directory under web/ 90 | */ 91 | public function testInvalidDestDirWebRoot() 92 | { 93 | $this->commandTester->execute(['--dest-dir' => 'web'], ['interactive' => false]); 94 | } 95 | 96 | /** 97 | * @expectedException \InvalidArgumentException 98 | * @expectedExceptionMessage 'dest-dir' must be a directory under web/ 99 | */ 100 | public function testInvalidDestDirWebRoot2() 101 | { 102 | $this->commandTester->execute(['--dest-dir' => 'web/'], ['interactive' => false]); 103 | } 104 | 105 | /** 106 | * @runInSeparateProcess 107 | */ 108 | public function testSourceTreeDefault() 109 | { 110 | $base = $this->baseDir; 111 | 112 | $this->commandTester->execute( 113 | ['--src-dir' => $base], 114 | ['interactive' => false] 115 | ); 116 | 117 | $this->assertFileExists("$base/images/.keep"); 118 | $this->assertFileExists("$base/scripts/app.js"); 119 | $this->assertFileExists("$base/stylesheets/app.scss"); 120 | $this->assertFileExists("$base/stylesheets/vendor.scss"); 121 | 122 | $this->assertFileNotExists("$base/stylesheets/app.less"); 123 | $this->assertFileNotExists("$base/scripts/app.coffee"); 124 | } 125 | 126 | /** 127 | * @runInSeparateProcess 128 | */ 129 | public function testSourceTreeLess() 130 | { 131 | $base = $this->baseDir; 132 | 133 | $this->commandTester->execute( 134 | [ 135 | '--src-dir' => $base, 136 | '--csspre' => 'less', 137 | ], 138 | ['interactive' => false] 139 | ); 140 | 141 | $this->assertFileExists("$base/stylesheets/app.less"); 142 | $this->assertFileExists("$base/stylesheets/vendor.less"); 143 | 144 | $this->assertFileNotExists("$base/stylesheets/app.scss"); 145 | } 146 | 147 | /** 148 | * @runInSeparateProcess 149 | */ 150 | public function testSourceTreeCoffee() 151 | { 152 | $base = $this->baseDir; 153 | 154 | $this->commandTester->execute( 155 | [ 156 | '--src-dir' => $base, 157 | '--coffee' => 'true', 158 | ], 159 | ['interactive' => false] 160 | ); 161 | 162 | $this->assertFileExists("$base/scripts/app.coffee"); 163 | 164 | $this->assertFileNotExists("$base/scripts/app.js"); 165 | } 166 | 167 | /** 168 | * @runInSeparateProcess 169 | */ 170 | public function testSourceTreeDryRun() 171 | { 172 | $base = $this->baseDir; 173 | 174 | $this->commandTester->execute([ 175 | '--src-dir' => $base, 176 | '--dry-run' => true, 177 | ], ['interactive' => false]); 178 | 179 | $this->assertFileNotExists("$base/images/.keep"); 180 | $this->assertFileNotExists("$base/scripts/.keep"); 181 | $this->assertFileNotExists("$base/stylesheets/.keep"); 182 | } 183 | 184 | /** 185 | * @runInSeparateProcess 186 | */ 187 | public function testCreateFilesDryRun() 188 | { 189 | $base = $this->baseDir; 190 | 191 | $this->commandTester->execute([ 192 | '--dry-run' => true, 193 | ], ['interactive' => false]); 194 | 195 | $this->assertRegExp("|Would have created file $base/gulpfile.js|", $this->commandTester->getDisplay()); 196 | $this->assertRegExp("|Would have created file $base/package.json|", $this->commandTester->getDisplay()); 197 | $this->assertRegExp("|Would have created file $base/bower.json|", $this->commandTester->getDisplay()); 198 | } 199 | 200 | /** 201 | * @runInSeparateProcess 202 | */ 203 | public function testCreateFilesExists() 204 | { 205 | $base = $this->baseDir; 206 | 207 | touch($base.'/gulpfile.js'); 208 | touch($base.'/package.json'); 209 | touch($base.'/bower.json'); 210 | 211 | $this->commandTester->execute([], ['interactive' => false]); 212 | 213 | $this->assertRegExp("|$base/gulpfile.js already exists|", $this->commandTester->getDisplay()); 214 | $this->assertRegExp("|$base/package.json already exists|", $this->commandTester->getDisplay()); 215 | $this->assertRegExp("|$base/bower.json already exists|", $this->commandTester->getDisplay()); 216 | } 217 | 218 | /** 219 | * @runInSeparateProcess 220 | */ 221 | public function testCreateFilesExistsForce() 222 | { 223 | $base = $this->baseDir; 224 | 225 | touch($base.'/gulpfile.js'); 226 | touch($base.'/package.json'); 227 | touch($base.'/bower.json'); 228 | 229 | $this->commandTester->execute([ 230 | '--force' => true, 231 | ], ['interactive' => false]); 232 | 233 | $this->assertRegExp("|Creating file $base/gulpfile.js|", $this->commandTester->getDisplay()); 234 | $this->assertRegExp("|Creating file $base/package.json|", $this->commandTester->getDisplay()); 235 | $this->assertRegExp("|Creating file $base/bower.json|", $this->commandTester->getDisplay()); 236 | } 237 | 238 | /** 239 | * @runInSeparateProcess 240 | */ 241 | public function testCreateFiles() 242 | { 243 | $base = $this->baseDir; 244 | 245 | $this->commandTester->execute([], ['interactive' => false]); 246 | 247 | $this->assertRegExp("|Creating file $base/gulpfile.js|", $this->commandTester->getDisplay()); 248 | $this->assertNotEmpty(file_get_contents("$base/gulpfile.js")); 249 | 250 | $this->assertRegExp("|Creating file $base/package.json|", $this->commandTester->getDisplay()); 251 | $this->assertNotEmpty(file_get_contents("$base/package.json")); 252 | 253 | $this->assertRegExp("|Creating file $base/bower.json|", $this->commandTester->getDisplay()); 254 | $this->assertNotEmpty(file_get_contents("$base/bower.json")); 255 | } 256 | 257 | /** 258 | * @param array $options 259 | * @param array $expected 260 | * @param bool $interactive 261 | */ 262 | private function assertOptions(array $options, array $expected, $interactive = true) 263 | { 264 | $defaults = !$interactive ? [] : [ 265 | 'src-dir' => 'bar', 266 | 'dest-dir' => 'web/bar', 267 | 'pipeline' => 'bar', 268 | 'csspre' => 'bar', 269 | 'coffee' => 'bar', 270 | ]; 271 | 272 | $options = array_merge($defaults, $options); 273 | $options['dry-run'] = true; 274 | 275 | foreach ($options as $key => $value) { 276 | if ($value !== null) { 277 | $options["--$key"] = $value; 278 | } 279 | 280 | unset($options[$key]); 281 | } 282 | 283 | // Simulate the user pressing enter. This is needed to test the default 284 | // value, has no impact when the value is provided. 285 | if (method_exists($this->commandTester, 'setInputs')) { 286 | $this->commandTester->setInputs([PHP_EOL]); 287 | } else { 288 | $stream = fopen('php://memory', 'r+', false); 289 | fputs($stream, PHP_EOL); 290 | rewind($stream); 291 | $this->command->getHelper('question')->setInputStream($stream); 292 | } 293 | 294 | $this->commandTester->execute($options, [ 295 | 'interactive' => $interactive, 296 | ]); 297 | 298 | foreach ($expected as $key => $value) { 299 | $this->assertEquals($value, $this->commandTester->getInput()->getOption($key)); 300 | } 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /Tests/DependencyInjection/Compiler/BaseCompilerPassTest.php: -------------------------------------------------------------------------------- 1 | registerPackagesService(); 18 | } 19 | 20 | /** 21 | * @expectedException \LogicException 22 | * @expectedExceptionMessage The Asset component is not registered in the container 23 | */ 24 | public function testThrowsExceptionIfAssetComponentIsNotRegistered() 25 | { 26 | $package = new Definition(); 27 | $package->addTag($this->namespaceService('package.asset'), ['alias' => 'foo']); 28 | $this->setDefinition('foo_service', $package); 29 | 30 | $this->container->removeDefinition('assets.packages'); 31 | 32 | $this->compile(); 33 | } 34 | 35 | /** 36 | * @expectedException \LogicException 37 | * @expectedExceptionMessage The tag for the service with id 'foo_service' must define an 'alias' attribute 38 | */ 39 | public function testThrowsExceptionIfPackageWithNoTag() 40 | { 41 | $package = new Definition(); 42 | $package->addTag($this->namespaceService('package.asset')); 43 | $this->setDefinition('foo_service', $package); 44 | 45 | $this->compile(); 46 | } 47 | 48 | /** 49 | * @expectedException \LogicException 50 | * @expectedExceptionMessage A package named 'foo' has already been registered 51 | */ 52 | public function testThrowsExceptionIfPackageIsAlreadyRegisteredWithAssetComponent() 53 | { 54 | $package = new Definition(); 55 | $package->addTag($this->namespaceService('package.asset'), ['alias' => 'foo']); 56 | $this->setDefinition('foo_service', $package); 57 | 58 | $this->container->removeDefinition('assets.packages'); 59 | 60 | $this->registerPackagesService([ 61 | new Reference('default_package'), 62 | ['foo', new Reference('foo_package')], 63 | ]); 64 | 65 | $this->compile(); 66 | } 67 | 68 | /** 69 | * @expectedException \LogicException 70 | * @expectedExceptionMessage Multiple packages were found with alias 'foo'. Package alias' must be unique 71 | */ 72 | public function testThrowsExceptionIfDuplicatePackage() 73 | { 74 | $package = new Definition(); 75 | $package->addTag($this->namespaceService('package.asset'), ['alias' => 'foo']); 76 | $this->setDefinition('foo_service', $package); 77 | 78 | $package2 = new Definition(); 79 | $package2->addTag($this->namespaceService('package.asset'), ['alias' => 'foo']); 80 | $this->setDefinition('foo_service_2', $package2); 81 | 82 | $this->compile(); 83 | } 84 | 85 | public function testPackageIsRegisteredIntoAssets() 86 | { 87 | $package = new Definition(); 88 | $package->addTag($this->namespaceService('package.asset'), ['alias' => 'foo']); 89 | $this->setDefinition('foo_service', $package); 90 | 91 | $this->compile(); 92 | 93 | $this->assertContainerBuilderHasServiceDefinitionWithMethodCall( 94 | 'assets.packages', 95 | 'addPackage', 96 | ['foo', new Reference('foo_service')] 97 | ); 98 | } 99 | 100 | public function testDefaultPackageIsRegisteredIntoAssets() 101 | { 102 | $package = new Definition(); 103 | $package->addTag($this->namespaceService('package.asset'), ['alias' => 'default']); 104 | $this->setDefinition('default_service', $package); 105 | 106 | $this->container->removeDefinition('assets.packages'); 107 | $this->registerPackagesService([ 108 | new Reference('default_package'), 109 | ['foo', new Reference('foo_package')], 110 | ]); 111 | 112 | $this 113 | ->registerService($this->namespaceService('package.fallback'), null) 114 | ->addArgument(['foo_pattern']) 115 | ->setPublic(false) 116 | ; 117 | 118 | $this->compile(); 119 | 120 | $this->assertContainerBuilderHasServiceDefinitionWithArgument( 121 | $this->namespaceService('package.fallback'), 122 | 0, 123 | ['foo_pattern'] 124 | ); 125 | 126 | $this->assertContainerBuilderHasServiceDefinitionWithMethodCall( 127 | $this->namespaceService('package.fallback'), 128 | 'setFallback', 129 | [new Reference('default_package')] 130 | ); 131 | 132 | $this->assertContainerBuilderHasServiceDefinitionWithArgument( 133 | 'assets.packages', 134 | 0, 135 | new Reference($this->namespaceService('package.fallback')) 136 | ); 137 | } 138 | 139 | /** 140 | * @param ContainerBuilder $container 141 | */ 142 | protected function registerCompilerPass(ContainerBuilder $container) 143 | { 144 | $container->addCompilerPass(new AssetCompilerPass()); 145 | } 146 | 147 | /** 148 | * {@inheritdoc} 149 | */ 150 | private function registerPackagesService($arguments = []) 151 | { 152 | $service = $this 153 | ->registerService('assets.packages', null) 154 | ->setPublic(false) 155 | ; 156 | 157 | foreach ($arguments as $argument) { 158 | $service->addArgument($argument); 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /Tests/DependencyInjection/ConfigurationTest.php: -------------------------------------------------------------------------------- 1 | assertConfigurationEquals( 13 | [], 14 | [ 15 | 'livereload' => [ 16 | 'enabled' => true, 17 | 'url' => '//localhost:35729/livereload.js', 18 | ], 19 | ], 20 | 'livereload' 21 | ); 22 | } 23 | 24 | public function testDefaultPackageFallbackPatterns() 25 | { 26 | $expected = [ 27 | 'fallback_patterns' => ['.*bundles\/.*'], 28 | ]; 29 | 30 | $this->assertConfigurationEquals([], $expected, 'fallback_patterns'); 31 | } 32 | 33 | public function testDefaultPackageDefaultPrefix() 34 | { 35 | $expected = ['prefix' => ['assets']]; 36 | 37 | $this->assertConfigurationEquals([], $expected, 'prefix'); 38 | } 39 | 40 | public function testDefaultPackageManifestIsDisabledByDefault() 41 | { 42 | $expected = $this->getManifestExpectedDefault(); 43 | 44 | $this->assertConfigurationEquals([], $expected, 'manifest'); 45 | } 46 | 47 | public function testDefaultPackageInferManifestPath() 48 | { 49 | $config = ['manifest' => true]; 50 | 51 | $expected = $this->getManifestExpectedDefault(); 52 | $expected['manifest']['enabled'] = true; 53 | $expected['manifest']['path'] = 'root_dir/../web/assets/manifest.json'; 54 | 55 | $this->assertConfigurationEquals($config, $expected, 'manifest'); 56 | } 57 | 58 | public function testDefaultPackageInferManifestPathWithPrefix() 59 | { 60 | $config = ['manifest' => true, 'prefix' => 'foo']; 61 | 62 | $expected = [ 63 | 'override_default_package' => true, 64 | 'fallback_patterns' => ['.*bundles\/.*'], 65 | 'livereload' => [ 66 | 'enabled' => true, 67 | 'url' => '//localhost:35729/livereload.js', 68 | ], 69 | 'prefix' => ['foo'], 70 | 'packages' => [], 71 | ]; 72 | 73 | $expected = array_merge($expected, $this->getManifestExpectedDefault()); 74 | $expected['manifest']['enabled'] = true; 75 | $expected['manifest']['path'] = 'root_dir/../web/foo/manifest.json'; 76 | 77 | $this->assertConfigurationEquals($config, $expected); 78 | } 79 | 80 | public function testPackagePrefixInvalidUrlAndPath() 81 | { 82 | $config = $this->getDefaultPackageConfig(); 83 | $config['packages']['app']['prefix'] = ['foo', 'http://foo']; 84 | 85 | $this->assertConfigurationInvalid($config, 'packages', 86 | '.*Packages cannot have both URL and path prefixes'); 87 | } 88 | 89 | public function testPackagePrefixInvalidMultiplePaths() 90 | { 91 | $config = $this->getDefaultPackageConfig(); 92 | $config['packages']['app']['prefix'] = ['foo', 'bar']; 93 | 94 | $this->assertConfigurationInvalid($config, 'packages', 95 | '.*Packages can only have one path prefix'); 96 | } 97 | 98 | public function testPackagePrefixString() 99 | { 100 | $config = $this->getDefaultPackageConfig(); 101 | $config['packages']['app']['prefix'] = 'foo'; 102 | 103 | $expected = $this->getPackagesExpectedDefault(); 104 | $expected['packages']['app']['prefix'] = ['foo']; 105 | 106 | $this->assertConfigurationEquals($config, $expected, 'packages'); 107 | } 108 | 109 | public function testPackagePrefixArray() 110 | { 111 | $config = $this->getDefaultPackageConfig(); 112 | $config['packages']['app']['prefix'] = ['http://foo', 'http://bar']; 113 | 114 | $expected = $this->getPackagesExpectedDefault(); 115 | $expected['packages']['app']['prefix'] = ['http://foo', 'http://bar']; 116 | 117 | $this->assertConfigurationEquals($config, $expected, 'packages'); 118 | } 119 | 120 | public function testPackageManifestIsDisabledByDefault() 121 | { 122 | $config = $this->getDefaultPackageConfig(); 123 | 124 | $expected = $this->getPackagesExpectedDefault(); 125 | $expected['packages']['app']['manifest']['enabled'] = false; 126 | $this->assertConfigurationEquals($config, $expected, 'packages'); 127 | } 128 | 129 | public function testPackageManifestUnsupportedFormat() 130 | { 131 | $config = $this->getDefaultPackageConfig(); 132 | $config['packages']['app']['manifest']['path'] = 'foo.json'; 133 | $config['packages']['app']['manifest']['format'] = 'yaml'; 134 | 135 | $this->assertConfigurationInvalid($config, 'packages', 136 | '.*For the moment only JSON manifest files are supported'); 137 | } 138 | 139 | public function testPackageManifestEnabled() 140 | { 141 | $config = $this->getDefaultPackageConfig(); 142 | $config['packages']['app']['manifest']['enabled'] = true; 143 | $config['packages']['app']['manifest']['path'] = 'foo.json'; 144 | 145 | $expected = $this->getPackagesExpectedDefault(); 146 | $expected['packages']['app']['manifest']['enabled'] = true; 147 | $expected['packages']['app']['manifest']['path'] = 'foo.json'; 148 | $this->assertConfigurationEquals($config, $expected, 'packages'); 149 | } 150 | 151 | public function testPackageManifestString() 152 | { 153 | $config = $this->getDefaultPackageConfig(); 154 | $config['packages']['app']['manifest'] = 'foo.json'; 155 | 156 | $expected = $this->getPackagesExpectedDefault(); 157 | $expected['packages']['app']['manifest']['enabled'] = true; 158 | $expected['packages']['app']['manifest']['path'] = 'foo.json'; 159 | $this->assertConfigurationEquals($config, $expected, 'packages'); 160 | } 161 | 162 | public function testPackageInferManifestPath() 163 | { 164 | $config = $this->getDefaultPackageConfig(); 165 | $config['packages']['app']['manifest'] = true; 166 | 167 | $expected = $this->getPackagesExpectedDefault(); 168 | $expected['packages']['app']['manifest']['enabled'] = true; 169 | $expected['packages']['app']['manifest']['path'] = 'root_dir/../web/app_prefix/manifest.json'; 170 | $this->assertConfigurationEquals($config, $expected, 'packages'); 171 | } 172 | 173 | /** 174 | * @return array 175 | */ 176 | private function getDefaultPackageConfig() 177 | { 178 | return [ 179 | 'packages' => [ 180 | 'app' => [ 181 | 'prefix' => 'app_prefix', 182 | ], 183 | ], 184 | ]; 185 | } 186 | 187 | /** 188 | * @return array 189 | */ 190 | private function getManifestExpectedDefault() 191 | { 192 | return [ 193 | 'manifest' => [ 194 | 'enabled' => false, 195 | 'format' => 'json', 196 | 'root_key' => null, 197 | ], 198 | ]; 199 | } 200 | 201 | /** 202 | * @return array 203 | */ 204 | private function getPackagesExpectedDefault() 205 | { 206 | return [ 207 | 'packages' => [ 208 | 'app' => [ 209 | 'prefix' => ['app_prefix'], 210 | 'manifest' => [ 211 | 'enabled' => false, 212 | 'format' => 'json', 213 | 'root_key' => null, 214 | ], 215 | ], 216 | ], 217 | ]; 218 | } 219 | 220 | /** 221 | * @param array $config 222 | * @param string $breadcrumbPath 223 | * @param null|string $expectedMessage 224 | */ 225 | protected function assertConfigurationInvalid(array $config, $breadcrumbPath, $expectedMessage = null) 226 | { 227 | parent::assertPartialConfigurationIsInvalid( 228 | [$config], 229 | $breadcrumbPath, 230 | "/$expectedMessage/", 231 | $useRegExp = true 232 | ); 233 | } 234 | 235 | /** 236 | * @param array $config 237 | * @param null|string $breadcrumbPath 238 | */ 239 | protected function assertConfigurationValid(array $config, $breadcrumbPath = null) 240 | { 241 | parent::assertConfigurationIsValid([$config], $breadcrumbPath); 242 | } 243 | 244 | /** 245 | * @param string $config 246 | * @param array $expected 247 | * @param null|string $breadcrumbPath 248 | */ 249 | protected function assertConfigurationEquals($config, array $expected, $breadcrumbPath = null) 250 | { 251 | $this->assertProcessedConfigurationEquals([$config], $expected, $breadcrumbPath); 252 | } 253 | 254 | /** 255 | * {@inheritdoc} 256 | */ 257 | protected function getConfiguration() 258 | { 259 | return new Configuration('root_dir'); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /Tests/DependencyInjection/RjFrontendExtensionAssetTest.php: -------------------------------------------------------------------------------- 1 | container->setParameter('kernel.root_dir', 'root_dir'); 12 | } 13 | 14 | public function testPathPackageIsRegistered() 15 | { 16 | $this->load(['packages' => [ 17 | 'app' => [ 18 | 'prefix' => 'foo', 19 | ], 20 | ]]); 21 | 22 | $package = $this->container->findDefinition($this->namespaceService('_package.app')); 23 | 24 | $this->assertEquals($package->getParent(), $this->namespaceService('asset.package.path')); 25 | $this->assertFalse($package->isPublic()); 26 | $this->assertEquals($package->getArgument(0), 'foo'); 27 | $this->assertEquals($package->getArgument(1), $this->namespaceService('version_strategy.empty')); 28 | 29 | $this->assertTrue($this->container->hasDefinition($this->namespaceService('version_strategy.empty'))); 30 | } 31 | 32 | public function testUrlPackageIsRegistered() 33 | { 34 | $this->load(['packages' => [ 35 | 'app' => [ 36 | 'prefix' => 'http://foo', 37 | ], 38 | ]]); 39 | 40 | $package = $this->container->findDefinition($this->namespaceService('_package.app')); 41 | 42 | $this->assertEquals($package->getParent(), $this->namespaceService('asset.package.url')); 43 | $this->assertFalse($package->isPublic()); 44 | $this->assertEquals($package->getArgument(0), ['http://foo']); 45 | $this->assertEquals($package->getArgument(1), $this->namespaceService('version_strategy.empty')); 46 | 47 | $this->assertTrue($this->container->hasDefinition($this->namespaceService('version_strategy.empty'))); 48 | } 49 | 50 | public function testPackageWithManifestIsRegistered() 51 | { 52 | $this->load(['packages' => [ 53 | 'app' => [ 54 | 'prefix' => 'foo', 55 | 'manifest' => [ 56 | 'path' => 'foo', 57 | ], 58 | ], 59 | ]]); 60 | 61 | $package = $this->container->findDefinition($this->namespaceService('_package.app')); 62 | $this->assertEquals($package->getArgument(1), $this->namespaceService('_package.app.version_strategy')); 63 | 64 | $vs = $this->container->findDefinition($this->namespaceService('_package.app.version_strategy')); 65 | $this->assertEquals($vs->getArgument(0), $this->namespaceService('_package.app.manifest_loader_cached')); 66 | } 67 | 68 | public function testPackageHasAliasTag() 69 | { 70 | $this->load(['packages' => [ 71 | 'app' => [ 72 | 'prefix' => 'foo', 73 | ], 74 | ]]); 75 | 76 | $package = $this->container->findDefinition($this->namespaceService('_package.app')); 77 | 78 | $this->assertTrue($package->hasTag($this->namespaceService('package.asset'))); 79 | 80 | $tag = $package->getTag($this->namespaceService('package.asset')); 81 | $tag = $tag[0]; 82 | $this->assertArrayHasKey('alias', $tag); 83 | $this->assertEquals('app', $tag['alias']); 84 | } 85 | 86 | public function testFallbackPackageIsRegistered() 87 | { 88 | $this->load(); 89 | 90 | $package = $this->container->findDefinition($this->namespaceService('package.fallback')); 91 | 92 | $this->assertFalse($package->isPublic()); 93 | $this->assertEquals($package->getArgument(0), ['.*bundles\/.*']); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Tests/DependencyInjection/RjFrontendExtensionBaseTest.php: -------------------------------------------------------------------------------- 1 | container->setParameter('kernel.root_dir', 'root_dir'); 12 | } 13 | 14 | public function testInjectLivereloadListenerIsRegistered() 15 | { 16 | $this->load([ 17 | 'livereload' => [ 18 | 'enabled' => true, 19 | 'url' => 'foo', 20 | ], 21 | ]); 22 | 23 | $this->assertContainerBuilderHasService( 24 | $this->namespaceService('livereload.listener'), 25 | 'Rj\FrontendBundle\EventListener\InjectLiveReloadListener' 26 | ); 27 | 28 | $this->assertContainerBuilderHasServiceDefinitionWithArgument( 29 | $this->namespaceService('livereload.listener'), 30 | $argumentIndex = 0, 31 | 'foo' 32 | ); 33 | } 34 | 35 | public function testInjectLivereloadListenerIsNotRegistered() 36 | { 37 | $this->load(['livereload' => false]); 38 | 39 | $this->assertContainerBuilderNotHasService($this->namespaceService('livereload.listener')); 40 | } 41 | 42 | public function testPackageIsRegisteredAndPrivate() 43 | { 44 | $this->load(['packages' => [ 45 | 'foo' => [ 46 | 'prefix' => 'foo_prefix', 47 | ], 48 | ]]); 49 | 50 | $this->assertContainerBuilderHasService($this->namespaceService('_package.foo')); 51 | $this->assertFalse($this->container->findDefinition($this->namespaceService('_package.foo'))->isPublic()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Tests/EventListener/InjectLiveReloadListenerTest.php: -------------------------------------------------------------------------------- 1 | '); 19 | $response->headers->set('X-Debug-Token', 'xxxxxxxx'); 20 | 21 | $listener = new InjectLiveReloadListener('bar'); 22 | $listener->onKernelResponse(new FilterResponseEvent( 23 | $this->mockKernel(), 24 | $this->mockRequest(), 25 | HttpKernelInterface::SUB_REQUEST, 26 | $response 27 | )); 28 | 29 | $this->assertEquals('foo', $response->getContent()); 30 | } 31 | 32 | public function testDontInjectScriptIfXmlHttpRequest() 33 | { 34 | $response = new Response('foo'); 35 | $response->headers->set('X-Debug-Token', 'xxxxxxxx'); 36 | 37 | $listener = new InjectLiveReloadListener('bar'); 38 | $listener->onKernelResponse(new FilterResponseEvent( 39 | $this->mockKernel(), 40 | $this->mockRequest(true), 41 | HttpKernelInterface::MASTER_REQUEST, 42 | $response 43 | )); 44 | 45 | $this->assertEquals('foo', $response->getContent()); 46 | } 47 | 48 | public function testDontInjectScriptIfXDebugTokenHeaderNotPresent() 49 | { 50 | $response = new Response('foo'); 51 | 52 | $listener = new InjectLiveReloadListener('bar'); 53 | $listener->onKernelResponse(new FilterResponseEvent( 54 | $this->mockKernel(), 55 | $this->mockRequest(), 56 | HttpKernelInterface::MASTER_REQUEST, 57 | $response 58 | )); 59 | 60 | $this->assertEquals('foo', $response->getContent()); 61 | } 62 | 63 | public function testDontInjectScriptIfNoClosingBodyTag() 64 | { 65 | $response = new Response('foo'); 66 | $response->headers->set('X-Debug-Token', 'xxxxxxxx'); 67 | 68 | $listener = new InjectLiveReloadListener('bar'); 69 | $listener->onKernelResponse(new FilterResponseEvent( 70 | $this->mockKernel(), 71 | $this->mockRequest(), 72 | HttpKernelInterface::MASTER_REQUEST, 73 | $response 74 | )); 75 | 76 | $this->assertEquals('foo', $response->getContent()); 77 | } 78 | 79 | public function testInjectScript() 80 | { 81 | $response = new Response('foo'); 82 | $response->headers->set('X-Debug-Token', 'xxxxxxxx'); 83 | 84 | $listener = new InjectLiveReloadListener('bar'); 85 | $listener->onKernelResponse(new FilterResponseEvent( 86 | $this->mockKernel(), 87 | $this->mockRequest(), 88 | HttpKernelInterface::MASTER_REQUEST, 89 | $response 90 | )); 91 | 92 | $this->assertEquals('foo', $response->getContent()); 93 | } 94 | 95 | /** 96 | * @param bool $isXmlHttpRequest 97 | * 98 | * @return Request|PHPUnit_Framework_MockObject_MockObject 99 | */ 100 | private function mockRequest($isXmlHttpRequest = false) 101 | { 102 | $request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request') 103 | ->disableOriginalConstructor() 104 | ->setMethods(['isXmlHttpRequest']) 105 | ->getMock() 106 | ; 107 | 108 | $request->expects($this->any()) 109 | ->method('isXmlHttpRequest') 110 | ->will($this->returnValue($isXmlHttpRequest)); 111 | 112 | return $request; 113 | } 114 | 115 | /** 116 | * @return Kernel|PHPUnit_Framework_MockObject_MockObject 117 | */ 118 | private function mockKernel() 119 | { 120 | return $this->getMockBuilder('Symfony\Component\HttpKernel\Kernel') 121 | ->disableOriginalConstructor() 122 | ->getMock() 123 | ; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Tests/Functional/BaseTestCase.php: -------------------------------------------------------------------------------- 1 | remove(sys_get_temp_dir().'/rj_frontend/'); 16 | } 17 | 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | protected static function createKernel(array $options = []) 22 | { 23 | return self::$kernel = new AppKernel($options); 24 | } 25 | 26 | /** 27 | * @param string $id 28 | * 29 | * @return object 30 | */ 31 | protected function get($id) 32 | { 33 | return $this->getContainer()->get($id); 34 | } 35 | 36 | /** 37 | * @return ContainerInterface 38 | */ 39 | protected function getContainer() 40 | { 41 | return self::$kernel->getContainer(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tests/Functional/InjectLivereloadTest.php: -------------------------------------------------------------------------------- 1 | createClient([ 13 | 'rj_frontend' => [ 14 | 'livereload' => false, 15 | ], 16 | ]); 17 | 18 | $router = $this->get('router'); 19 | 20 | $client->request('GET', $router->generate('livereload_inject')); 21 | 22 | $response = $client->getResponse()->getContent(); 23 | $this->assertEquals('foo', $response); 24 | } 25 | 26 | /** 27 | * @runInSeparateProcess 28 | */ 29 | public function testEnabled() 30 | { 31 | $client = $this->createClient([ 32 | 'rj_frontend' => [ 33 | 'livereload' => [ 34 | 'enabled' => true, 35 | 'url' => '://foo', 36 | ], 37 | ], 38 | ]); 39 | 40 | $router = $this->get('router'); 41 | 42 | $client->request('GET', $router->generate('livereload_inject')); 43 | 44 | $response = $client->getResponse()->getContent(); 45 | $this->assertEquals('foo', $response); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Tests/Functional/PackagesTest.php: -------------------------------------------------------------------------------- 1 | doTest('packages_default', '/css/foo.css', [ 13 | 'rj_frontend' => [ 14 | 'override_default_package' => false, 15 | ], 16 | ]); 17 | } 18 | 19 | /** 20 | * @runInSeparateProcess 21 | */ 22 | public function testDefaultPackage() 23 | { 24 | $this->doTest('packages_default', '/foo/css/foo.css', [ 25 | 'rj_frontend' => [ 26 | 'prefix' => 'foo', 27 | ], 28 | ]); 29 | } 30 | 31 | /** 32 | * @runInSeparateProcess 33 | */ 34 | public function testDefaultPackageWithManifest() 35 | { 36 | $manifest = tempnam('/tmp', ''); 37 | 38 | file_put_contents($manifest, json_encode([ 39 | 'css/foo.css' => 'css/foo-123.css', 40 | ])); 41 | 42 | $this->doTest('packages_default', '/app_prefix/css/foo-123.css', [ 43 | 'rj_frontend' => [ 44 | 'prefix' => 'app_prefix', 45 | 'manifest' => [ 46 | 'enabled' => true, 47 | 'path' => $manifest, 48 | ], 49 | ], 50 | ]); 51 | 52 | unlink($manifest); 53 | } 54 | 55 | /** 56 | * @runInSeparateProcess 57 | */ 58 | public function testDefaultPackageWithManifestWithInferredPath() 59 | { 60 | // it uses the manifest file in TestApp/web/assets/manifest.json 61 | 62 | $this->doTest('packages_default', '/assets/css/foo-123.css', [ 63 | 'rj_frontend' => [ 64 | 'manifest' => [ 65 | 'enabled' => true, 66 | ], 67 | ], 68 | ]); 69 | } 70 | 71 | /** 72 | * @runInSeparateProcess 73 | */ 74 | public function testFallbackPackage() 75 | { 76 | $this->doTest('packages_fallback', '/bundles/foo.css', [ 77 | 'rj_frontend' => [ 78 | 'prefix' => 'foo', 79 | ], 80 | ]); 81 | } 82 | 83 | /** 84 | * @runInSeparateProcess 85 | */ 86 | public function testPathPackage() 87 | { 88 | $this->doTest('packages_custom', '/app_prefix/css/foo.css', [ 89 | 'rj_frontend' => [ 90 | 'packages' => [ 91 | 'app' => [ 92 | 'prefix' => 'app_prefix', 93 | ], 94 | ], 95 | ], 96 | ]); 97 | } 98 | 99 | /** 100 | * @runInSeparateProcess 101 | */ 102 | public function testUrlPackage() 103 | { 104 | $this->doTest('packages_custom', 'http://foo/css/foo.css', [ 105 | 'rj_frontend' => [ 106 | 'packages' => [ 107 | 'app' => [ 108 | 'prefix' => 'http://foo', 109 | ], 110 | ], 111 | ], 112 | ]); 113 | } 114 | 115 | /** 116 | * @runInSeparateProcess 117 | */ 118 | public function testUrlPackageSsl() 119 | { 120 | $this->doTest('packages_custom', 'https://foo/css/foo.css', [ 121 | 'rj_frontend' => [ 122 | 'packages' => [ 123 | 'app' => [ 124 | 'prefix' => 'https://foo', 125 | ], 126 | ], 127 | ], 128 | ]); 129 | } 130 | 131 | /** 132 | * @runInSeparateProcess 133 | */ 134 | public function testUrlPackageNoProtocol() 135 | { 136 | $this->doTest('packages_custom', '//foo/css/foo.css', [ 137 | 'rj_frontend' => [ 138 | 'packages' => [ 139 | 'app' => [ 140 | 'prefix' => '//foo', 141 | ], 142 | ], 143 | ], 144 | ]); 145 | } 146 | 147 | /** 148 | * @runInSeparateProcess 149 | */ 150 | public function testPackageWithManifest() 151 | { 152 | $manifest = tempnam('/tmp', ''); 153 | 154 | file_put_contents($manifest, json_encode([ 155 | 'css/foo.css' => 'css/foo-123.css', 156 | ])); 157 | 158 | $this->doTest('packages_custom', '/app_prefix/css/foo-123.css', [ 159 | 'rj_frontend' => [ 160 | 'packages' => [ 161 | 'app' => [ 162 | 'prefix' => 'app_prefix', 163 | 'manifest' => [ 164 | 'enabled' => true, 165 | 'path' => $manifest, 166 | ], 167 | ], 168 | ], 169 | ], 170 | ]); 171 | 172 | unlink($manifest); 173 | } 174 | 175 | /** 176 | * @param string $route 177 | * @param string $expected 178 | * @param array $config 179 | */ 180 | private function doTest($route, $expected, $config = []) 181 | { 182 | $client = $this->createClient($config); 183 | $router = $this->get('router'); 184 | 185 | $client->request('GET', $router->generate($route)); 186 | 187 | $response = $client->getResponse()->getContent(); 188 | $this->assertEquals($expected, $response); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /Tests/Functional/TestApp/TestBundle/Controller/InjectLivereloadController.php: -------------------------------------------------------------------------------- 1 | '); 13 | 14 | $response->headers->set('X-Debug-Token', 'xxxxxxxx'); 15 | 16 | return $response; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/Functional/TestApp/TestBundle/Controller/PackagesController.php: -------------------------------------------------------------------------------- 1 | render('TestBundle:Packages:custom.html.php'); 12 | } 13 | 14 | public function defaultAction() 15 | { 16 | return $this->render('TestBundle:Packages:default.html.php'); 17 | } 18 | 19 | public function fallbackAction() 20 | { 21 | return $this->render('TestBundle:Packages:fallback.html.php'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Tests/Functional/TestApp/TestBundle/Resources/views/Packages/custom.html.php: -------------------------------------------------------------------------------- 1 | getUrl('css/foo.css', 'app'); 2 | -------------------------------------------------------------------------------- /Tests/Functional/TestApp/TestBundle/Resources/views/Packages/default.html.php: -------------------------------------------------------------------------------- 1 | getUrl('css/foo.css'); 2 | -------------------------------------------------------------------------------- /Tests/Functional/TestApp/TestBundle/Resources/views/Packages/fallback.html.php: -------------------------------------------------------------------------------- 1 | getUrl('bundles/foo.css'); 2 | -------------------------------------------------------------------------------- /Tests/Functional/TestApp/TestBundle/TestBundle.php: -------------------------------------------------------------------------------- 1 | config = $config; 15 | 16 | parent::__construct('test', true); 17 | } 18 | 19 | public function registerBundles() 20 | { 21 | return [ 22 | new \Symfony\Bundle\FrameworkBundle\FrameworkBundle(), 23 | new \Rj\FrontendBundle\RjFrontendBundle(), 24 | new \Rj\FrontendBundle\Tests\Functional\TestApp\TestBundle\TestBundle(), 25 | ]; 26 | } 27 | 28 | public function registerContainerConfiguration(LoaderInterface $loader) 29 | { 30 | $loader->load(__DIR__.'/config/config.yml'); 31 | 32 | foreach ($this->config as $key => $values) { 33 | $loader->load(function ($container) use ($key, $values) { 34 | $container->loadFromExtension($key, $values); 35 | }); 36 | } 37 | } 38 | 39 | public function getCacheDir() 40 | { 41 | return sys_get_temp_dir().'/rj_frontend'; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tests/Functional/TestApp/app/config/config.yml: -------------------------------------------------------------------------------- 1 | framework: 2 | secret: test 3 | test: ~ 4 | session: 5 | storage_id: session.storage.mock_file 6 | form: false 7 | csrf_protection: false 8 | validation: false 9 | router: 10 | resource: "%kernel.root_dir%/config/routing.yml" 11 | templating: 12 | engines: [php] 13 | -------------------------------------------------------------------------------- /Tests/Functional/TestApp/app/config/routing.yml: -------------------------------------------------------------------------------- 1 | livereload_inject: 2 | path: /livereload/inject 3 | defaults: { _controller: TestBundle:InjectLivereload:inject } 4 | 5 | packages_custom: 6 | path: /packages/custom 7 | defaults: { _controller: TestBundle:Packages:custom } 8 | 9 | packages_default: 10 | path: /packages/default 11 | defaults: { _controller: TestBundle:Packages:default } 12 | 13 | packages_fallback: 14 | path: /packages/fallback 15 | defaults: { _controller: TestBundle:Packages:fallback } 16 | -------------------------------------------------------------------------------- /Tests/Functional/TestApp/web/assets/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "css/foo.css": "css/foo-123.css" 3 | } 4 | -------------------------------------------------------------------------------- /Tests/Manifest/Loader/CachedManifestLoaderTest.php: -------------------------------------------------------------------------------- 1 | path = tempnam('/tmp', ''); 19 | } 20 | 21 | public function tearDown() 22 | { 23 | unlink($this->path); 24 | } 25 | 26 | public function testLoad() 27 | { 28 | $manifest = $this->load(['foo.css' => 'foo-123.css']); 29 | 30 | $this->assertEquals('foo-123.css', $manifest->get('foo.css')); 31 | } 32 | 33 | public function testLoadRootKey() 34 | { 35 | $manifest = $this->load(['foo.css' => 'foo-123.css'], 'assets'); 36 | 37 | $this->assertEquals('foo-123.css', $manifest->get('foo.css')); 38 | } 39 | 40 | /** 41 | * @expectedException \InvalidArgumentException 42 | */ 43 | public function testLoadRootKeyNotFound() 44 | { 45 | file_put_contents($this->path, json_encode(['foo.css' => 'foo-123.css'])); 46 | 47 | $loader = new JsonManifestLoader($this->path, 'assets'); 48 | $loader->load(); 49 | } 50 | 51 | public function testGetPath() 52 | { 53 | $loader = new JsonManifestLoader($this->path); 54 | 55 | $this->assertEquals($this->path, $loader->getPath()); 56 | } 57 | 58 | /** 59 | * @param array $entries 60 | * @param null|string $rootKey 61 | * 62 | * @return Manifest 63 | */ 64 | private function load(array $entries, $rootKey = null) 65 | { 66 | if ($rootKey) { 67 | $entries = [$rootKey => $entries]; 68 | } 69 | 70 | file_put_contents($this->path, json_encode($entries)); 71 | 72 | $loader = new JsonManifestLoader($this->path, $rootKey); 73 | 74 | return $loader->load(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Tests/Manifest/ManifestTest.php: -------------------------------------------------------------------------------- 1 | manifest = new Manifest([ 18 | 'foo.css' => 'foo-123.css', 19 | 'bar.js' => 'bar-123.js', 20 | ]); 21 | } 22 | 23 | public function testHas() 24 | { 25 | $this->assertFalse($this->manifest->has('foo')); 26 | $this->assertTrue($this->manifest->has('foo.css')); 27 | } 28 | 29 | public function testGet() 30 | { 31 | $this->assertEmpty($this->manifest->get('foo')); 32 | $this->assertEquals('foo-123.css', $this->manifest->get('foo.css')); 33 | } 34 | 35 | public function testAll() 36 | { 37 | $entries = $this->manifest->all(); 38 | 39 | $this->assertCount(2, $entries); 40 | $this->assertEquals('foo-123.css', $entries['foo.css']); 41 | $this->assertEquals('bar-123.js', $entries['bar.js']); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tests/Package/FallbackPackageTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder('Symfony\Component\Asset\PathPackage'); 29 | $defaultBuilder = $this->getMockBuilder('Symfony\Component\Asset\PathPackage'); 30 | 31 | $this->package = $packageBuilder 32 | ->disableOriginalConstructor() 33 | ->getMock() 34 | ; 35 | 36 | $this->default = $defaultBuilder 37 | ->disableOriginalConstructor() 38 | ->getMock() 39 | ; 40 | 41 | $this->fallbackPackage = new FallbackPackage( 42 | ['must_fallback\/'], 43 | $this->package 44 | ); 45 | 46 | $this->fallbackPackage->setFallback($this->default); 47 | } 48 | 49 | public function testGetVersion() 50 | { 51 | $this->package 52 | ->method('getVersion') 53 | ->willReturn('package') 54 | ; 55 | 56 | $this->default 57 | ->method('getVersion') 58 | ->willReturn('default') 59 | ; 60 | 61 | $this->assertEquals('package', $this->fallbackPackage->getVersion('css/foo.css')); 62 | $this->assertEquals('default', $this->fallbackPackage->getVersion('must_fallback/foo.css')); 63 | } 64 | 65 | public function testGetUrl() 66 | { 67 | $this->package 68 | ->method('getUrl') 69 | ->willReturn('package') 70 | ; 71 | 72 | $this->default 73 | ->method('getUrl') 74 | ->willReturn('default') 75 | ; 76 | 77 | $this->assertEquals('package', $this->fallbackPackage->getUrl('css/foo.css')); 78 | $this->assertEquals('default', $this->fallbackPackage->getUrl('must_fallback/foo.css')); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Tests/VersionStrategy/ManifestVersionStrategyTest.php: -------------------------------------------------------------------------------- 1 | 'foo-123.css', 17 | ])); 18 | 19 | $vs = new ManifestVersionStrategy(new JsonManifestLoader($jsonFile)); 20 | 21 | $this->assertEquals('foo-123.css', $vs->applyVersion('foo.css')); 22 | $this->assertEquals('bar.css', $vs->applyVersion('bar.css')); 23 | 24 | unlink($jsonFile); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Util/Util.php: -------------------------------------------------------------------------------- 1 | loader = $loader; 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function getVersion($path) 33 | { 34 | return ''; 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | public function applyVersion($path) 41 | { 42 | if ($this->manifest === null) { 43 | $this->manifest = $this->loader->load(); 44 | } 45 | 46 | if (!$this->manifest->has($path)) { 47 | return $path; 48 | } 49 | 50 | return $this->manifest->get($path); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "regularjack/frontend-bundle", 3 | "type": "symfony-bundle", 4 | "description": "A modern frontend development workflow for Symfony apps", 5 | "keywords": ["frontend", "front-end", "bower", "gulp", "livereload", "npm"], 6 | "homepage": "https://github.com/regularjack/frontend-bundle", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Paulo Rodrigues Pinto", 11 | "email": "regularjack@gmail.com" 12 | }, 13 | { 14 | "name": "Contributors", 15 | "homepage": "https://github.com/regularjack/frontend-bundle/contributors" 16 | } 17 | ], 18 | "config": { 19 | "sort-packages": true 20 | }, 21 | "autoload": { 22 | "psr-4": { "Rj\\FrontendBundle\\": "" } 23 | }, 24 | "require": { 25 | "php": ">=5.4", 26 | "symfony/asset": "~2.7|~3.0", 27 | "symfony/framework-bundle": "~2.7|~3.0", 28 | "symfony/console": "~2.7|~3.0", 29 | "symfony/config": "~2.7|~3.0", 30 | "symfony/dependency-injection": "~2.7|~3.0", 31 | "symfony/event-dispatcher": "~2.7|~3.0", 32 | "symfony/http-kernel": "~2.7|~3.0", 33 | "symfony/process": "~2.7|~3.0", 34 | "symfony/templating": "~2.7|~3.0" 35 | }, 36 | "require-dev": { 37 | "matthiasnoback/symfony-config-test": "~1.4", 38 | "matthiasnoback/symfony-dependency-injection-test": "~0.7", 39 | "phpunit/phpunit": ">=4.2", 40 | "symfony/browser-kit": "^2.7|~3.0", 41 | "symfony/filesystem": "~2.7|~3.0", 42 | "symfony/http-foundation": "~2.7|~3.0", 43 | "symfony/phpunit-bridge": "~2.7|~3.0", 44 | "symfony/routing": "~2.7|~3.0", 45 | "symfony/var-dumper": "~2.7|~3.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ./Tests 7 | ./Tests/Functional/TestApp 8 | 9 | 10 | 11 | 12 | ./ 13 | 14 | ./vendor 15 | ./Tests 16 | ./Resources 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx==1.5.4 2 | sphinx-autobuild==0.6.0 3 | sphinx-rtd-theme==0.2.4 4 | -e git+https://github.com/fabpot/sphinx-php.git@f1b69df331217d34425aa09821449e08fa219d38#egg=sphinx_php 5 | --------------------------------------------------------------------------------