├── Classes ├── Integration │ ├── JsonDecodeException.php │ ├── IdGeneratorInterface.php │ ├── JsonDecoderInterface.php │ ├── IdGenerator.php │ ├── PackageFactoryInterface.php │ ├── FixedIdGenerator.php │ ├── SettingsServiceInterface.php │ ├── FilesystemInterface.php │ ├── AssetRegistryInterface.php │ ├── JsonDecoder.php │ ├── EntryLookupFactoryInterface.php │ ├── TypoScriptFrontendControllerEventListener.php │ ├── PackageFactory.php │ ├── Filesystem.php │ ├── SettingsService.php │ ├── AssetRegistry.php │ ├── EntryLookupFactory.php │ └── PageRendererHooks.php ├── Asset │ ├── UndefinedBuildException.php │ ├── EntrypointNotFoundException.php │ ├── EntrypointLookupCollectionInterface.php │ ├── EntrypointLookupInterface.php │ ├── IntegrityDataProviderInterface.php │ ├── TagRendererInterface.php │ ├── EntrypointLookupCollection.php │ ├── EntrypointLookup.php │ └── TagRenderer.php ├── ValueObject │ ├── JsonPackage.php │ ├── File.php │ ├── FileType.php │ ├── LinkTag.php │ └── ScriptTag.php ├── ViewHelpers │ ├── Stimulus │ │ ├── TargetViewHelper.php │ │ ├── ControllerViewHelper.php │ │ ├── ActionViewHelper.php │ │ ├── Dto │ │ │ ├── StimulusTargetsDto.php │ │ │ ├── AbstractStimulusDto.php │ │ │ ├── StimulusActionsDto.php │ │ │ └── StimulusControllersDto.php │ │ └── AbstractViewHelper.php │ ├── WebpackCssFilesViewHelper.php │ ├── WebpackJsFilesViewHelper.php │ ├── PrefetchViewHelper.php │ ├── PrerenderViewHelper.php │ ├── DnsPrefetchViewHelper.php │ ├── PreconnectViewHelper.php │ ├── PreloadViewHelper.php │ ├── RenderWebpackLinkTagsViewHelper.php │ ├── AssetViewHelper.php │ ├── RenderWebpackScriptTagsViewHelper.php │ └── SvgViewHelper.php ├── Form │ └── FormDataProvider │ │ └── RichtextEncoreConfiguration.php └── Middleware │ └── AssetsMiddleware.php ├── Configuration ├── TCA │ └── Overrides │ │ └── sys_template.php ├── TypoScript │ ├── constants.typoscript │ └── setup.typoscript ├── RequestMiddlewares.php └── Services.php ├── ext_emconf.php ├── Resources ├── Public │ └── Icons │ │ └── Extension.svg └── Private │ └── Templates │ └── Favicons.html ├── ext_localconf.php ├── composer.json ├── README.md └── LICENSE.md /Classes/Integration/JsonDecodeException.php: -------------------------------------------------------------------------------- 1 | 'TYPO3 with Webpack Encore', 5 | 'description' => 'Webpack Encore from Symfony for TYPO3', 6 | 'category' => 'fe', 7 | 'author' => 'Sebastian Schreiber', 8 | 'author_email' => 'breakpoint@schreibersebastian.de', 9 | 'state' => 'stable', 10 | 'version' => '6.0.0', 11 | 'constraints' => [ 12 | 'depends' => [ 13 | 'typo3' => '12.4.0-13.4.99', 14 | ], 15 | 'conflicts' => [], 16 | 'suggests' => [], 17 | ], 18 | 'autoload' => [ 19 | 'psr-4' => ['Ssch\\Typo3Encore\\' => 'Classes'] 20 | ], 21 | ]; 22 | -------------------------------------------------------------------------------- /Classes/Integration/FixedIdGenerator.php: -------------------------------------------------------------------------------- 1 | id; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Resources/Public/Icons/Extension.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Classes/Integration/SettingsServiceInterface.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'ssch/typo3-encore-handler' => [ 15 | 'target' => Ssch\Typo3Encore\Middleware\AssetsMiddleware::class, 16 | 'description' => 'Add HTTP/2 Push functionality for assets managed by encore', 17 | 'after' => ['typo3/cms-frontend/prepare-tsfe-rendering'], 18 | ], 19 | ], 20 | ]; 21 | -------------------------------------------------------------------------------- /Classes/Integration/FilesystemInterface.php: -------------------------------------------------------------------------------- 1 | manifestJsonPath; 27 | } 28 | 29 | public function getUrl(string $path): string 30 | { 31 | return $this->package->getUrl($path); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Classes/Asset/IntegrityDataProviderInterface.php: -------------------------------------------------------------------------------- 1 | 'sha384-Q86c+opr0lBUPWN28BLJFqmLhho+9ZcJpXHorQvX6mYDWJ24RQcdDarXFQYN8HLc', 22 | * 'path/to/styles.css' => 'sha384-ymG7OyjISWrOpH9jsGvajKMDEOP/mKJq8bHC0XdjQA6P8sg2nu+2RLQxcNNwE/3J', ] 23 | * 24 | * @return string[] 25 | */ 26 | public function getIntegrityData(): array; 27 | } 28 | -------------------------------------------------------------------------------- /ext_localconf.php: -------------------------------------------------------------------------------- 1 | renderPreProcess'; 8 | // Add collected assets to page cache 9 | }, 'typo3_encore'); 10 | 11 | $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['formDataGroup']['tcaDatabaseRecord'][\Ssch\Typo3Encore\Form\FormDataProvider\RichtextEncoreConfiguration::class] = [ 12 | 'depends' => [\TYPO3\CMS\Backend\Form\FormDataProvider\TcaText::class], 13 | ]; 14 | 15 | $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['formDataGroup']['flexFormSegment'][\Ssch\Typo3Encore\Form\FormDataProvider\RichtextEncoreConfiguration::class] = [ 16 | 'depends' => [\TYPO3\CMS\Backend\Form\FormDataProvider\TcaText::class], 17 | ]; 18 | -------------------------------------------------------------------------------- /Classes/ValueObject/File.php: -------------------------------------------------------------------------------- 1 | file; 27 | } 28 | 29 | public function getType(): string 30 | { 31 | return $this->fileType->getType(); 32 | } 33 | 34 | public function getAttributes(): array 35 | { 36 | return $this->attributes; 37 | } 38 | 39 | public function getRel(): string 40 | { 41 | return $this->rel; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Classes/Asset/TagRendererInterface.php: -------------------------------------------------------------------------------- 1 | registerArgument('controllerName', 'string|array', 'The Stimulus controller name to render.', true); 22 | $this->registerArgument( 23 | 'targetNames', 24 | 'string', 25 | 'The space-separated list of target names if a string is passed to the 1st argument. Optional.', 26 | ); 27 | } 28 | 29 | public function render(): string 30 | { 31 | return $this->renderStimulusTarget( 32 | $this->arguments['controllerName'], 33 | $this->arguments['targetNames'] 34 | )->__toString(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Classes/Integration/TypoScriptFrontendControllerEventListener.php: -------------------------------------------------------------------------------- 1 | assetRegistry->getRegisteredFiles(); 27 | if ($registeredFiles === []) { 28 | return; 29 | } 30 | 31 | $event->getController() 32 | ->config['encore_asset_registry'] = [ 33 | 'registered_files' => $this->assetRegistry->getRegisteredFiles(), 34 | 'default_attributes' => $this->assetRegistry->getDefaultAttributes(), 35 | 'settings' => $this->settingsService->getSettings(), 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/Stimulus/ControllerViewHelper.php: -------------------------------------------------------------------------------- 1 | registerArgument('controllerName', 'string|array', 'The Stimulus controller name to render.', true); 19 | $this->registerArgument( 20 | 'controllerValues', 21 | 'array', 22 | 'array of data if a string is passed to the 1st argument', 23 | false, 24 | [] 25 | ); 26 | $this->registerArgument( 27 | 'controllerClasses', 28 | 'array', 29 | 'Array of classes to add to the controller', 30 | false, 31 | [] 32 | ); 33 | } 34 | 35 | public function render(): string 36 | { 37 | return $this->renderStimulusController( 38 | $this->arguments['controllerName'], 39 | $this->arguments['controllerValues'], 40 | $this->arguments['controllerClasses'] 41 | )->__toString(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Classes/ValueObject/FileType.php: -------------------------------------------------------------------------------- 1 | type = $type; 40 | } 41 | 42 | public static function createStyle(): self 43 | { 44 | return new self(self::STYLE); 45 | } 46 | 47 | public static function createScript(): self 48 | { 49 | return new self(self::SCRIPT); 50 | } 51 | 52 | public static function createFromString(string $type): self 53 | { 54 | return new self($type); 55 | } 56 | 57 | public function getType(): string 58 | { 59 | return $this->type; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/WebpackCssFilesViewHelper.php: -------------------------------------------------------------------------------- 1 | registerArgument('entryName', 'string', 'The entry name', true); 28 | $this->registerArgument( 29 | 'buildName', 30 | 'string', 31 | 'The build name', 32 | false, 33 | EntrypointLookupInterface::DEFAULT_BUILD 34 | ); 35 | } 36 | 37 | public function render(): array 38 | { 39 | return $this->entrypointLookupCollection->getEntrypointLookup($this->arguments['buildName'])->getCssFiles( 40 | $this->arguments['entryName'] 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/WebpackJsFilesViewHelper.php: -------------------------------------------------------------------------------- 1 | registerArgument('entryName', 'string', 'The entry name', true); 28 | $this->registerArgument( 29 | 'buildName', 30 | 'string', 31 | 'The build name', 32 | false, 33 | EntrypointLookupInterface::DEFAULT_BUILD 34 | ); 35 | } 36 | 37 | public function render(): array 38 | { 39 | return $this->entrypointLookupCollection->getEntrypointLookup( 40 | $this->arguments['buildName'] 41 | )->getJavaScriptFiles($this->arguments['entryName']); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Classes/Integration/PackageFactory.php: -------------------------------------------------------------------------------- 1 | filesystem->getFileAbsFileName( 34 | $this->settingsService->getStringByPath($manifestJsonPath) 35 | ); 36 | 37 | return new JsonPackage($absoluteManifestJsonPath, new Package(new JsonManifestVersionStrategy( 38 | $absoluteManifestJsonPath 39 | ))); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/Stimulus/ActionViewHelper.php: -------------------------------------------------------------------------------- 1 | registerArgument('controllerName', 'string|array', 'The Stimulus controller name to render.', true); 22 | $this->registerArgument( 23 | 'eventName', 24 | 'string', 25 | 'The event to listen to trigger if a string is passed to the 1st argument. Optional.', 26 | ); 27 | $this->registerArgument( 28 | 'actionName', 29 | 'string', 30 | 'The action to trigger if a string is passed to the 1st argument. Optional.' 31 | ); 32 | $this->registerArgument('parameters', 'array', 'Parameters to pass to the action. Optional.', false, []); 33 | } 34 | 35 | public function render(): string 36 | { 37 | return $this->renderStimulusAction( 38 | $this->arguments['controllerName'], 39 | $this->arguments['actionName'], 40 | $this->arguments['eventName'], 41 | $this->arguments['parameters'] 42 | )->__toString(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Classes/ValueObject/LinkTag.php: -------------------------------------------------------------------------------- 1 | entryName; 32 | } 33 | 34 | public function getMedia(): string 35 | { 36 | return $this->media; 37 | } 38 | 39 | public function getBuildName(): string 40 | { 41 | return $this->buildName; 42 | } 43 | 44 | public function getPageRenderer(): ?PageRenderer 45 | { 46 | return $this->pageRenderer; 47 | } 48 | 49 | public function getParameters(): array 50 | { 51 | return $this->parameters; 52 | } 53 | 54 | public function isRegisterFile(): bool 55 | { 56 | return $this->registerFile; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/Stimulus/Dto/StimulusTargetsDto.php: -------------------------------------------------------------------------------- 1 | targets) === 0) { 21 | return ''; 22 | } 23 | 24 | return implode( 25 | ' ', 26 | array_map(fn (string $attribute, string $value): string => $attribute . '="' . $this->escapeAsHtmlAttr( 27 | $value 28 | ) . '"', array_keys($this->targets), $this->targets) 29 | ); 30 | } 31 | 32 | /** 33 | * @param string $controllerName the Stimulus controller name 34 | * @param string|null $targetNames The space-separated list of target names if a string is passed to the 1st argument. Optional. 35 | */ 36 | public function addTarget(string $controllerName, ?string $targetNames = null): void 37 | { 38 | $controllerName = $this->getFormattedControllerName($controllerName); 39 | 40 | $this->targets['data-' . $controllerName . '-target'] = $targetNames; 41 | } 42 | 43 | public function toArray(): array 44 | { 45 | return $this->targets; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/PrefetchViewHelper.php: -------------------------------------------------------------------------------- 1 | registerArgument('uri', 'string', 'The uri to prefetch', true); 29 | $this->registerArgument('as', 'string', 'The type like style or script', true); 30 | $this->registerArgument( 31 | 'attributes', 32 | 'array', 33 | 'The attributes of this link (e.g. "[\'as\' => true]", "[\'pr\' => 0.5]")', 34 | false, 35 | [] 36 | ); 37 | } 38 | 39 | public function render(): void 40 | { 41 | $attributes = $this->arguments['attributes'] ?? []; 42 | $file = new File($this->arguments['uri'], FileType::createFromString( 43 | $this->arguments['as'] 44 | ), $attributes, 'prefetch'); 45 | $this->assetRegistry->registerFile($file); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/PrerenderViewHelper.php: -------------------------------------------------------------------------------- 1 | registerArgument('uri', 'string', 'The uri to prerender', true); 29 | $this->registerArgument('as', 'string', 'The type like style or script', true); 30 | $this->registerArgument( 31 | 'attributes', 32 | 'array', 33 | 'The attributes of this link (e.g. "[\'as\' => true]", "[\'pr\' => 0.5]")', 34 | false, 35 | [] 36 | ); 37 | } 38 | 39 | public function render(): void 40 | { 41 | $attributes = $this->arguments['attributes'] ?? []; 42 | $file = new File($this->arguments['uri'], FileType::createFromString( 43 | $this->arguments['as'] 44 | ), $attributes, 'prerender'); 45 | $this->assetRegistry->registerFile($file); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/DnsPrefetchViewHelper.php: -------------------------------------------------------------------------------- 1 | registerArgument('uri', 'string', 'The uri to dsn-prefetch', true); 29 | $this->registerArgument('as', 'string', 'The type like style or script', true); 30 | $this->registerArgument( 31 | 'attributes', 32 | 'array', 33 | 'The attributes of this link (e.g. "[\'as\' => true]", "[\'pr\' => 0.5]")', 34 | false, 35 | [] 36 | ); 37 | } 38 | 39 | public function render(): void 40 | { 41 | $attributes = $this->arguments['attributes'] ?? []; 42 | $file = new File($this->arguments['uri'], FileType::createFromString( 43 | $this->arguments['as'] 44 | ), $attributes, 'dns-prefetch'); 45 | 46 | $this->assetRegistry->registerFile($file); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/PreconnectViewHelper.php: -------------------------------------------------------------------------------- 1 | registerArgument('uri', 'string', 'The uri to preconnect', true); 29 | $this->registerArgument('as', 'string', 'The type like style or script', true); 30 | $this->registerArgument( 31 | 'attributes', 32 | 'array', 33 | 'The attributes of this link (e.g. "[\'as\' => true]", "[\'pr\' => 0.5]")', 34 | false, 35 | [] 36 | ); 37 | } 38 | 39 | public function render(): void 40 | { 41 | $attributes = $this->arguments['attributes'] ?? []; 42 | 43 | $file = new File($this->arguments['uri'], FileType::createFromString( 44 | $this->arguments['as'] 45 | ), $attributes, 'preconnect'); 46 | 47 | $this->assetRegistry->registerFile($file); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/PreloadViewHelper.php: -------------------------------------------------------------------------------- 1 | registerArgument('uri', 'string', 'The uri to preload', true); 29 | $this->registerArgument('as', 'string', 'The type like style or script', true); 30 | $this->registerArgument( 31 | 'attributes', 32 | 'array', 33 | 'The attributes of this link (e.g. "[\'as\' => true]", "[\'crossorigin\' => \'use-credentials\']")', 34 | false, 35 | [] 36 | ); 37 | } 38 | 39 | public function render(): void 40 | { 41 | $attributes = $this->arguments['attributes'] ?? []; 42 | $file = new File($this->arguments['uri'], FileType::createFromString( 43 | $this->arguments['as'] 44 | ), $attributes, 'preload'); 45 | $this->assetRegistry->registerFile($file); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Classes/ValueObject/ScriptTag.php: -------------------------------------------------------------------------------- 1 | entryName; 32 | } 33 | 34 | public function getPosition(): string 35 | { 36 | return $this->position; 37 | } 38 | 39 | public function getBuildName(): string 40 | { 41 | return $this->buildName; 42 | } 43 | 44 | public function getPageRenderer(): ?PageRenderer 45 | { 46 | return $this->pageRenderer; 47 | } 48 | 49 | public function getParameters(): array 50 | { 51 | return $this->parameters; 52 | } 53 | 54 | public function isRegisterFile(): bool 55 | { 56 | return $this->registerFile; 57 | } 58 | 59 | public function isLibrary(): bool 60 | { 61 | return $this->isLibrary; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Classes/Integration/Filesystem.php: -------------------------------------------------------------------------------- 1 | escapeAsHtmlAttr($this->normalizeControllerName($controllerName)); 21 | } 22 | 23 | protected function getFormattedValue(mixed $value): string 24 | { 25 | if ((\is_object($value) && \is_callable([$value, '__toString']))) { 26 | $value = (string) $value; 27 | } elseif (! \is_scalar($value)) { 28 | $value = json_encode($value); 29 | } elseif (\is_bool($value)) { 30 | $value = $value ? 'true' : 'false'; 31 | } 32 | 33 | return (string) $value; 34 | } 35 | 36 | protected function escapeAsHtmlAttr(string $value): string 37 | { 38 | return htmlspecialchars($value, ENT_QUOTES); 39 | } 40 | 41 | /** 42 | * Normalize a Stimulus controller name into its HTML equivalent (no special character and / becomes --). 43 | * 44 | * @see https://stimulus.hotwired.dev/reference/controllers 45 | */ 46 | private function normalizeControllerName(string $controllerName): string 47 | { 48 | return (string) preg_replace('/^@/', '', str_replace('_', '-', str_replace('/', '--', $controllerName))); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Classes/Asset/EntrypointLookupCollection.php: -------------------------------------------------------------------------------- 1 | buildEntrypoints === null) { 32 | $this->buildEntrypoints = $this->entryLookupFactory->getCollection(); 33 | } 34 | if ($buildName === null) { 35 | if ($this->defaultBuildName === null) { 36 | throw new UndefinedBuildException( 37 | 'There is no default build configured: please pass an argument to getEntrypointLookup().' 38 | ); 39 | } 40 | 41 | $buildName = $this->defaultBuildName; 42 | } 43 | 44 | if (! isset($this->buildEntrypoints[$buildName])) { 45 | throw new UndefinedBuildException(sprintf('The build "%s" is not configured', $buildName)); 46 | } 47 | 48 | return $this->buildEntrypoints[$buildName]; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Configuration/Services.php: -------------------------------------------------------------------------------- 1 | services(); 23 | $services->defaults() 24 | ->public() 25 | ->autowire() 26 | ->autoconfigure(); 27 | 28 | $services->load('Ssch\\Typo3Encore\\', __DIR__ . '/../Classes/')->exclude([ 29 | __DIR__ . '/../Classes/ValueObject', 30 | __DIR__ . '/../Classes/Asset/EntrypointLookup.php', 31 | ]); 32 | 33 | $services->alias(IdGeneratorInterface::class, IdGenerator::class); 34 | $services->set(FixedIdGenerator::class)->args(['fixed']); 35 | $services->set(TypoScriptFrontendControllerEventListener::class)->tag('event.listener', [ 36 | 'event' => AfterCacheableContentIsGeneratedEvent::class, 37 | ]); 38 | 39 | if (Environment::getContext()->isTesting()) { 40 | $services->alias(IdGeneratorInterface::class, FixedIdGenerator::class); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/RenderWebpackLinkTagsViewHelper.php: -------------------------------------------------------------------------------- 1 | registerArgument('entryName', 'string', 'The entry name', true); 29 | $this->registerArgument('media', 'string', 'Media type', false, 'all'); 30 | $this->registerArgument( 31 | 'buildName', 32 | 'string', 33 | 'The build name', 34 | false, 35 | EntrypointLookupInterface::DEFAULT_BUILD 36 | ); 37 | $this->registerArgument('parameters', 'array', 'Additional parameters for the PageRenderer', false, []); 38 | $this->registerArgument('registerFile', 'bool', 'Register file for HTTP/2 push functionality', false, true); 39 | } 40 | 41 | public function render(): void 42 | { 43 | $linkTag = new LinkTag( 44 | $this->arguments['entryName'], 45 | $this->arguments['media'], 46 | $this->arguments['buildName'], 47 | null, 48 | $this->arguments['parameters'], 49 | $this->arguments['registerFile'] 50 | ); 51 | $this->tagRenderer->renderWebpackLinkTags($linkTag); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Classes/Integration/SettingsService.php: -------------------------------------------------------------------------------- 1 | settings === null) { 29 | $this->settings = $this->configurationManager->getConfiguration( 30 | ConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS, 31 | 'Typo3Encore' 32 | ); 33 | } 34 | 35 | return $this->settings; 36 | } 37 | 38 | public function getArrayByPath(string $path): array 39 | { 40 | return (array) $this->getByPath($path); 41 | } 42 | 43 | public function getStringByPath(string $path): string 44 | { 45 | return (string) $this->getByPath($path); 46 | } 47 | 48 | public function getBooleanByPath(string $path): bool 49 | { 50 | return (bool) $this->getByPath($path); 51 | } 52 | 53 | /** 54 | * Returns the settings at path $path, which is separated by ".", e.g. "pages.uid". "pages.uid" would return 55 | * $this->settings['pages']['uid']. 56 | * 57 | * If the path is invalid or no entry is found, false is returned. 58 | * 59 | * @return mixed 60 | */ 61 | private function getByPath(string $path) 62 | { 63 | return ObjectAccess::getPropertyPath($this->getSettings(), $path); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Classes/Integration/AssetRegistry.php: -------------------------------------------------------------------------------- 1 | reset(); 27 | } 28 | 29 | public function registerFile(File $file): void 30 | { 31 | $rel = $file->getRel(); 32 | $type = $file->getType(); 33 | $fileName = $file->getFile(); 34 | $attributes = $file->getAttributes(); 35 | 36 | if (! isset($this->registeredFiles[$rel])) { 37 | $this->registeredFiles[$rel] = []; 38 | } 39 | 40 | if (! isset($this->registeredFiles[$rel]['files'][$type])) { 41 | $this->registeredFiles[$rel]['files'][$type] = []; 42 | } 43 | 44 | $file = GeneralUtility::createVersionNumberedFilename($fileName); 45 | $this->registeredFiles[$rel]['files'][$type][$file] = $attributes; 46 | } 47 | 48 | public function getRegisteredFiles(): array 49 | { 50 | return $this->registeredFiles; 51 | } 52 | 53 | public function getDefaultAttributes(): array 54 | { 55 | if (count($this->defaultAttributes) === 0) { 56 | $this->defaultAttributes['crossorigin'] = $this->settingsService->getStringByPath('preload.crossorigin'); 57 | } 58 | return $this->defaultAttributes; 59 | } 60 | 61 | private function reset(): void 62 | { 63 | $this->registeredFiles = []; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/Stimulus/Dto/StimulusActionsDto.php: -------------------------------------------------------------------------------- 1 | actions) === 0) { 23 | return ''; 24 | } 25 | 26 | return rtrim( 27 | 'data-action="' . implode(' ', $this->actions) . '" ' . implode(' ', array_map( 28 | fn (string $attribute, string $value): string => $attribute . '="' . $this->escapeAsHtmlAttr( 29 | $value 30 | ) . '"', 31 | array_keys($this->parameters), 32 | $this->parameters 33 | )) 34 | ); 35 | } 36 | 37 | /** 38 | * @param array $parameters Parameters to pass to the action. Optional. 39 | */ 40 | public function addAction( 41 | string $controllerName, 42 | string $actionName, 43 | ?string $eventName = null, 44 | array $parameters = [] 45 | ): void { 46 | $controllerName = $this->getFormattedControllerName($controllerName); 47 | $action = $controllerName . '#' . $this->escapeAsHtmlAttr($actionName); 48 | 49 | if ($eventName !== null) { 50 | $action = $eventName . '->' . $action; 51 | } 52 | 53 | $this->actions[] = $action; 54 | 55 | foreach ($parameters as $name => $value) { 56 | $this->parameters['data-' . $controllerName . '-' . $name . '-param'] = $this->getFormattedValue($value); 57 | } 58 | } 59 | 60 | public function toArray(): array 61 | { 62 | return [ 63 | 'data-action' => implode(' ', $this->actions), 64 | ] + $this->parameters; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/AssetViewHelper.php: -------------------------------------------------------------------------------- 1 | registerArgument('pathToFile', 'string', 'The path to the file', true); 32 | $this->registerArgument( 33 | 'package', 34 | 'string', 35 | 'The package configuration to use', 36 | false, 37 | EntrypointLookupInterface::DEFAULT_BUILD 38 | ); 39 | } 40 | 41 | public function render(): string 42 | { 43 | $jsonPackage = $this->packageFactory->getPackage($this->arguments['package']); 44 | 45 | return $jsonPackage->getUrl( 46 | $this->getRelativeFilePath($this->arguments['pathToFile'], $jsonPackage->getManifestJsonPath()) 47 | ); 48 | } 49 | 50 | private function getRelativeFilePath(string $pathToFile, string $absolutePathToManifestJson): string 51 | { 52 | if (! PathUtility::isExtensionPath($pathToFile)) { 53 | return $pathToFile; 54 | } 55 | 56 | $absolutePathToFile = $this->filesystem->getFileAbsFileName($pathToFile); 57 | 58 | if (\str_starts_with($absolutePathToFile, Environment::getPublicPath())) { 59 | return PathUtility::stripPathSitePrefix($absolutePathToFile); 60 | } 61 | 62 | return substr($absolutePathToFile, strlen(dirname($absolutePathToManifestJson) . '/')); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/RenderWebpackScriptTagsViewHelper.php: -------------------------------------------------------------------------------- 1 | registerArgument('entryName', 'string', 'The entry name', true); 29 | $this->registerArgument( 30 | 'position', 31 | 'string', 32 | 'The position to render the files', 33 | false, 34 | TagRendererInterface::POSITION_FOOTER 35 | ); 36 | $this->registerArgument( 37 | 'buildName', 38 | 'string', 39 | 'The build name', 40 | false, 41 | EntrypointLookupInterface::DEFAULT_BUILD 42 | ); 43 | $this->registerArgument('parameters', 'array', 'Additional parameters for the PageRenderer', false, []); 44 | $this->registerArgument('registerFile', 'bool', 'Register file for HTTP/2 push functionality', false, true); 45 | $this->registerArgument( 46 | 'isLibrary', 47 | 'bool', 48 | 'Defines if the entry should be loaded as a library (i.e. before other files)', 49 | false, 50 | false 51 | ); 52 | } 53 | 54 | public function render(): void 55 | { 56 | $scriptTag = new ScriptTag( 57 | $this->arguments['entryName'], 58 | $this->arguments['position'], 59 | $this->arguments['buildName'], 60 | null, 61 | $this->arguments['parameters'], 62 | $this->arguments['registerFile'], 63 | $this->arguments['isLibrary'] 64 | ); 65 | 66 | $this->tagRenderer->renderWebpackScriptTags($scriptTag); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Classes/Integration/EntryLookupFactory.php: -------------------------------------------------------------------------------- 1 | settingsService->getArrayByPath('builds'); 41 | $entrypointsPathDefaultBuild = $this->settingsService->getStringByPath('entrypointJsonPath'); 42 | $strictMode = $this->settingsService->getBooleanByPath('strictMode'); 43 | 44 | $builds = []; 45 | 46 | if (count($buildConfigurations) > 0) { 47 | foreach ($buildConfigurations as $buildConfigurationKey => $buildConfiguration) { 48 | $entrypointsPath = sprintf('%s/entrypoints.json', $buildConfiguration); 49 | $builds[$buildConfigurationKey] = $this->createEntrypointLookUp($entrypointsPath, $strictMode); 50 | } 51 | } 52 | 53 | if ($this->filesystem->exists($this->filesystem->getFileAbsFileName($entrypointsPathDefaultBuild))) { 54 | $builds['_default'] = $this->createEntrypointLookUp($entrypointsPathDefaultBuild, $strictMode); 55 | } 56 | 57 | self::$collection = $builds; 58 | 59 | return $builds; 60 | } 61 | 62 | private function createEntrypointLookUp( 63 | string $entrypointJsonPath, 64 | bool $strictMode 65 | ): EntrypointLookupInterface { 66 | return new EntrypointLookup($entrypointJsonPath, $strictMode, $this->jsonDecoder, $this->filesystem); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssch/typo3-encore", 3 | "type": "typo3-cms-extension", 4 | "description": "Use Webpack Encore in TYPO3 Context", 5 | "license": "GPL-2.0-or-later", 6 | "keywords": [ 7 | "encore", 8 | "webpack" 9 | ], 10 | "authors": [ 11 | { 12 | "name": "Sebastian Schreiber", 13 | "email": "breakpoint@schreibersebastian.de", 14 | "role": "Developer" 15 | } 16 | ], 17 | "prefer-stable": true, 18 | "minimum-stability": "dev", 19 | "require": { 20 | "php": ">=8.1", 21 | "typo3/cms-core": "^12.4 || ^13.4", 22 | "symfony/web-link": "^6.0 || ^7.0", 23 | "symfony/asset": "^6.0 || ^7.0", 24 | "ext-dom": "*", 25 | "typo3/cms-tstemplate": "^12.4 || ^13.4", 26 | "webmozart/assert": "^1.10" 27 | }, 28 | "require-dev": { 29 | "phpstan/phpstan": "^1.0", 30 | "typo3/testing-framework": "^8.0 || ^9.0", 31 | "typo3/minimal": "^12.4 || ^13.4", 32 | "php-parallel-lint/php-parallel-lint": "^1.3", 33 | "phpspec/prophecy-phpunit": "^2.0", 34 | "rector/rector": "^1.2.8", 35 | "phpstan/phpstan-webmozart-assert": "^1.2.2", 36 | "phpstan/phpstan-phpunit": "^1.0", 37 | "jangregor/phpstan-prophecy": "^1.0", 38 | "phpstan/extension-installer": "^1.1", 39 | "saschaegerer/phpstan-typo3": "^1.8.0", 40 | "symplify/easy-coding-standard": "^12.1", 41 | "phpstan/phpstan-strict-rules": "^1.4.4", 42 | "typo3/cms-rte-ckeditor": "^12.4 || ^13.4", 43 | "typo3/cms-install": "^12.4 || ^13.4" 44 | }, 45 | "replace": { 46 | "typo3-ter/typo3-encore": "self.version" 47 | }, 48 | "autoload": { 49 | "psr-4": { 50 | "Ssch\\Typo3Encore\\": "Classes" 51 | } 52 | }, 53 | "autoload-dev": { 54 | "psr-4": { 55 | "Ssch\\Typo3Encore\\Tests\\": "Tests" 56 | } 57 | }, 58 | "config": { 59 | "vendor-dir": ".Build/vendor", 60 | "bin-dir": ".Build/bin", 61 | "preferred-install": { 62 | "typo3/cms": "source", 63 | "typo3/cms-core": "source", 64 | "*": "dist" 65 | }, 66 | "allow-plugins": { 67 | "phpstan/extension-installer": true, 68 | "typo3/class-alias-loader": true, 69 | "typo3/cms-composer-installers": true 70 | } 71 | }, 72 | "scripts": { 73 | "analyze": "phpstan --memory-limit=-1", 74 | "test-php-lint": [ 75 | ".Build/bin/parallel-lint ./Classes/", 76 | ".Build/bin/parallel-lint ./Configuration/TCA/", 77 | ".Build/bin/parallel-lint ./Tests/" 78 | ], 79 | "lint-php": "parallel-lint Tests Classes Configuration", 80 | "check-style": "ecs check --ansi", 81 | "fix-style": [ 82 | "ecs check --fix --ansi" 83 | ], 84 | "test-unit": [ 85 | ".Build/bin/phpunit --configuration Build/UnitTests.xml" 86 | ], 87 | "test-functional": [ 88 | ".Build/bin/phpunit --configuration Build/FunctionalTests.xml" 89 | ] 90 | }, 91 | "extra": { 92 | "branch-alias": { 93 | "dev-master": "6.x-dev" 94 | }, 95 | "typo3/cms": { 96 | "extension-key": "typo3_encore", 97 | "web-dir": ".Build/Web" 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/Stimulus/Dto/StimulusControllersDto.php: -------------------------------------------------------------------------------- 1 | controllers) === 0) { 25 | return ''; 26 | } 27 | 28 | return rtrim( 29 | 'data-controller="' . implode(' ', $this->controllers) . '" ' . 30 | implode( 31 | ' ', 32 | array_map(fn (string $attribute, string $value): string => $attribute . '="' . $this->escapeAsHtmlAttr( 33 | $value 34 | ) . '"', array_keys($this->values), $this->values) 35 | ) . ' ' . 36 | implode( 37 | ' ', 38 | array_map(fn (string $attribute, string $value): string => $attribute . '="' . $this->escapeAsHtmlAttr( 39 | $value 40 | ) . '"', array_keys($this->classes), $this->classes) 41 | ) 42 | ); 43 | } 44 | 45 | public function addController( 46 | string $controllerName, 47 | array $controllerValues = [], 48 | array $controllerClasses = [] 49 | ): void { 50 | $controllerName = $this->getFormattedControllerName($controllerName); 51 | 52 | $this->controllers[] = $controllerName; 53 | 54 | foreach ($controllerValues as $key => $value) { 55 | if ($value === null) { 56 | continue; 57 | } 58 | 59 | $key = $this->escapeAsHtmlAttr($this->normalizeKeyName($key)); 60 | $value = $this->getFormattedValue($value); 61 | 62 | $this->values['data-' . $controllerName . '-' . $key . '-value'] = $value; 63 | } 64 | 65 | foreach ($controllerClasses as $key => $class) { 66 | $key = $this->escapeAsHtmlAttr($this->normalizeKeyName($key)); 67 | 68 | $this->values['data-' . $controllerName . '-' . $key . '-class'] = $class; 69 | } 70 | } 71 | 72 | public function toArray(): array 73 | { 74 | if (\count($this->controllers) === 0) { 75 | return []; 76 | } 77 | 78 | return [ 79 | 'data-controller' => implode(' ', $this->controllers), 80 | ] + $this->values + $this->classes; 81 | } 82 | 83 | private function normalizeKeyName(string $str): string 84 | { 85 | // Adapted from ByteString::camel 86 | $str = ucfirst(str_replace(' ', '', ucwords((string) preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $str)))); 87 | 88 | // Adapted from ByteString::snake 89 | return strtolower((string) preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], '\1-\2', $str)); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Classes/Form/FormDataProvider/RichtextEncoreConfiguration.php: -------------------------------------------------------------------------------- 1 | entrypointLookupCollection = $entrypointLookupCollection ?? GeneralUtility::makeInstance( 27 | EntrypointLookupCollection::class 28 | ); 29 | } 30 | 31 | public function addData(array $result): array 32 | { 33 | foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) { 34 | if (! isset($fieldConfig['config']['type']) || $fieldConfig['config']['type'] !== 'text') { 35 | continue; 36 | } 37 | 38 | if (! isset($fieldConfig['config']['enableRichtext']) || (bool) $fieldConfig['config']['enableRichtext'] !== true) { 39 | continue; 40 | } 41 | 42 | $rteConfiguration = $fieldConfig['config']['richtextConfiguration']; 43 | 44 | // replace contentsCss with correct path 45 | if (! isset($rteConfiguration['editor']['config']['contentsCss'])) { 46 | continue; 47 | } 48 | $contentsCss = (array) $rteConfiguration['editor']['config']['contentsCss']; 49 | 50 | $updatedContentCss = []; 51 | foreach ($contentsCss as $cssFile) { 52 | $updatedContent = $this->getContentCss($cssFile); 53 | $updatedContentCss[] = is_array($updatedContent) ? $updatedContent : [$updatedContent]; 54 | } 55 | $contentsCss = array_merge(...$updatedContentCss); 56 | 57 | $result['processedTca']['columns'][$fieldName]['config']['richtextConfiguration']['editor']['config']['contentsCss'] = $contentsCss; 58 | } 59 | 60 | return $result; 61 | } 62 | 63 | private function getContentCss(string $contentsCss): array 64 | { 65 | if (! str_starts_with($contentsCss, 'typo3_encore:')) { 66 | // keep the css file as-is 67 | return [$contentsCss]; 68 | } 69 | 70 | // strip prefix 71 | $cssFile = substr($contentsCss, strlen('typo3_encore:')); 72 | $buildAndEntryName = GeneralUtility::trimExplode(':', $cssFile, true, 2); 73 | $buildName = EntrypointLookupInterface::DEFAULT_BUILD; 74 | 75 | if (count($buildAndEntryName) === 2) { 76 | [$buildName, $entryName] = $buildAndEntryName; 77 | } else { 78 | $entryName = $buildAndEntryName[0]; 79 | } 80 | 81 | $entryPointLookup = $this->entrypointLookupCollection->getEntrypointLookup($buildName); 82 | $cssFiles = $entryPointLookup->getCssFiles($entryName); 83 | // call reset() to allow multiple RTEs on the same page. 84 | // Otherwise only the first RTE will have the CSS. 85 | $entryPointLookup->reset(); 86 | 87 | return $cssFiles; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Classes/Integration/PageRendererHooks.php: -------------------------------------------------------------------------------- 1 | $jsFile) { 50 | if (! $this->isEncoreEntryName($jsFile['file'])) { 51 | continue; 52 | } 53 | 54 | $buildAndEntryName = $this->createBuildAndEntryName($jsFile['file']); 55 | $buildName = EntrypointLookupInterface::DEFAULT_BUILD; 56 | 57 | if (count($buildAndEntryName) === 2) { 58 | [$buildName, $entryName] = $buildAndEntryName; 59 | } else { 60 | $entryName = $buildAndEntryName[0]; 61 | } 62 | 63 | $position = ($jsFile['section'] ?? '') === self::PART_FOOTER ? TagRendererInterface::POSITION_FOOTER : ''; 64 | 65 | unset($params[$includeType][$key], $jsFile['file'], $jsFile['section'], $jsFile['integrity']); 66 | 67 | $scriptTag = new ScriptTag($entryName, $position, $buildName, $pageRenderer, $jsFile, true, $isLibrary); 68 | 69 | $this->tagRenderer->renderWebpackScriptTags($scriptTag); 70 | } 71 | } 72 | 73 | // Add CSS-Files by entryNames 74 | foreach (TagRendererInterface::ALLOWED_CSS_POSITIONS as $includeType) { 75 | if (! isset($params[$includeType])) { 76 | continue; 77 | } 78 | 79 | foreach ($params[$includeType] as $key => $cssFile) { 80 | if (! $this->isEncoreEntryName($cssFile['file'])) { 81 | continue; 82 | } 83 | $buildAndEntryName = $this->createBuildAndEntryName($cssFile['file']); 84 | $buildName = EntrypointLookupInterface::DEFAULT_BUILD; 85 | 86 | if (count($buildAndEntryName) === 2) { 87 | [$buildName, $entryName] = $buildAndEntryName; 88 | } else { 89 | $entryName = $buildAndEntryName[0]; 90 | } 91 | 92 | unset($params[$includeType][$key], $cssFile['file']); 93 | 94 | $linkTag = new LinkTag($entryName, 'all', $buildName, $pageRenderer, $cssFile); 95 | $this->tagRenderer->renderWebpackLinkTags($linkTag); 96 | } 97 | } 98 | } 99 | 100 | private function isEncoreEntryName(string $file): bool 101 | { 102 | return \str_starts_with($file, self::ENCORE_PREFIX); 103 | } 104 | 105 | private function removePrefix(string $file): string 106 | { 107 | return str_replace(self::ENCORE_PREFIX, '', $file); 108 | } 109 | 110 | private function createBuildAndEntryName(string $file): array 111 | { 112 | return GeneralUtility::trimExplode(':', $this->removePrefix($file), true, 2); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Resources/Private/Templates/Favicons.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Classes/Asset/EntrypointLookup.php: -------------------------------------------------------------------------------- 1 | entrypointJsonPath = $filesystem->getFileAbsFileName($entrypointJsonPath); 36 | $this->filesystem = $filesystem; 37 | } 38 | 39 | public function getJavaScriptFiles(string $entryName): array 40 | { 41 | return $this->getEntryFiles($entryName, 'js'); 42 | } 43 | 44 | public function getCssFiles(string $entryName): array 45 | { 46 | return $this->getEntryFiles($entryName, 'css'); 47 | } 48 | 49 | public function getIntegrityData(): array 50 | { 51 | $entriesData = $this->getEntriesData(); 52 | 53 | if (! array_key_exists('integrity', $entriesData)) { 54 | return []; 55 | } 56 | 57 | return $entriesData['integrity']; 58 | } 59 | 60 | /** 61 | * Resets the state of this service. 62 | */ 63 | public function reset(): void 64 | { 65 | $this->returnedFiles = []; 66 | } 67 | 68 | private function getEntryFiles(string $entryName, string $key): array 69 | { 70 | $this->validateEntryName($entryName); 71 | $entriesData = $this->getEntriesData(); 72 | 73 | if (! isset($entriesData['entrypoints'][$entryName][$key])) { 74 | // If we don't find the file type then just send back nothing. 75 | return []; 76 | } 77 | 78 | // make sure to not return the same file multiple times 79 | $entryFiles = $entriesData['entrypoints'][$entryName][$key]; 80 | $newFiles = array_values(array_diff($entryFiles, $this->returnedFiles)); 81 | $this->returnedFiles = array_merge($this->returnedFiles, $newFiles); 82 | 83 | return $newFiles; 84 | } 85 | 86 | private function validateEntryName(string $entryName): void 87 | { 88 | $entriesData = $this->getEntriesData(); 89 | if (! isset($entriesData['entrypoints'][$entryName]) && $this->strictMode) { 90 | $withoutExtension = substr($entryName, 0, (int) strrpos($entryName, '.')); 91 | 92 | if (isset($entriesData['entrypoints'][$withoutExtension])) { 93 | throw new EntrypointNotFoundException(sprintf( 94 | 'Could not find the entry "%s". Try "%s" instead (without the extension).', 95 | $entryName, 96 | $withoutExtension 97 | )); 98 | } 99 | 100 | throw new EntrypointNotFoundException(sprintf( 101 | 'Could not find the entry "%s" in "%s". Found: %s.', 102 | $entryName, 103 | $this->entrypointJsonPath, 104 | implode(', ', array_keys($entriesData)) 105 | )); 106 | } 107 | } 108 | 109 | private function getEntriesData(): array 110 | { 111 | if ($this->entriesData !== null) { 112 | return $this->entriesData; 113 | } 114 | 115 | if (! $this->filesystem->exists($this->entrypointJsonPath)) { 116 | throw new InvalidArgumentException(sprintf( 117 | 'Could not find the entrypoints file from Webpack: the file "%s" does not exist.', 118 | $this->entrypointJsonPath 119 | )); 120 | } 121 | 122 | try { 123 | $this->entriesData = $this->jsonDecoder->decode($this->filesystem->get($this->entrypointJsonPath)); 124 | } catch (JsonDecodeException) { 125 | throw new InvalidArgumentException(sprintf( 126 | 'There was a problem JSON decoding the "%s" file', 127 | $this->entrypointJsonPath 128 | )); 129 | } 130 | 131 | if (! isset($this->entriesData['entrypoints'])) { 132 | throw new InvalidArgumentException(sprintf( 133 | 'Could not find an "entrypoints" key in the "%s" file', 134 | $this->entrypointJsonPath 135 | )); 136 | } 137 | 138 | return $this->entriesData; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Classes/Middleware/AssetsMiddleware.php: -------------------------------------------------------------------------------- 1 | controller = $GLOBALS['TSFE']; 45 | } 46 | 47 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 48 | { 49 | $response = $handler->handle($request); 50 | 51 | if (($response instanceof NullResponse)) { 52 | return $response; 53 | } 54 | 55 | $registeredFiles = $this->collectRegisteredFiles(); 56 | 57 | if ($registeredFiles === []) { 58 | return $response; 59 | } 60 | 61 | $linkProvider = $request->getAttribute('_links'); 62 | if ($linkProvider === null) { 63 | $request = $request->withAttribute('_links', new GenericLinkProvider()); 64 | } 65 | 66 | /** @var GenericLinkProvider $linkProvider */ 67 | $linkProvider = $request->getAttribute('_links'); 68 | $defaultAttributes = $this->collectDefaultAttributes(); 69 | $crossOrigin = $defaultAttributes['crossorigin'] ? (bool) $defaultAttributes['crossorigin'] : false; 70 | 71 | foreach ($registeredFiles as $rel => $relFiles) { 72 | // You can disable or enable one of the resource hints via typoscript simply by adding something like that preload.enable = 1, dns-prefetch.enable = 1 73 | if ($this->getBooleanConfigByPath(sprintf('%s.enable', $rel)) === false) { 74 | continue; 75 | } 76 | 77 | foreach ($relFiles['files'] as $type => $files) { 78 | foreach ($files as $href => $attributes) { 79 | $link = (new Link($rel, PathUtility::getAbsoluteWebPath($href)))->withAttribute('as', $type); 80 | if ($this->canAddCrossOriginAttribute($crossOrigin, $rel)) { 81 | $link = $link->withAttribute('crossorigin', $crossOrigin); 82 | } 83 | 84 | foreach ($attributes as $key => $value) { 85 | $link = $link->withAttribute($key, $value); 86 | } 87 | 88 | $linkProvider = $linkProvider->withLink($link); 89 | } 90 | } 91 | } 92 | 93 | $request = $request->withAttribute('_links', $linkProvider); 94 | 95 | /** @var GenericLinkProvider $linkProvider */ 96 | $linkProvider = $request->getAttribute('_links'); 97 | 98 | if ($linkProvider->getLinks() !== []) { 99 | /** @var LinkInterface[]|Traversable $links */ 100 | $links = $linkProvider->getLinks(); 101 | $serializedLinks = (new HttpHeaderSerializer())->serialize($links); 102 | 103 | if (! is_string($serializedLinks)) { 104 | throw new UnexpectedValueException('Could not serialize the links'); 105 | } 106 | 107 | $response = $response->withHeader('Link', $serializedLinks); 108 | } 109 | 110 | return $response; 111 | } 112 | 113 | private function canAddCrossOriginAttribute(bool $crossOrigin, string $rel): bool 114 | { 115 | return $crossOrigin !== false && (string) $crossOrigin !== '' && in_array( 116 | $rel, 117 | self::$crossOriginAllowed, 118 | true 119 | ); 120 | } 121 | 122 | private function collectRegisteredFiles(): array 123 | { 124 | return array_replace( 125 | $this->controller->config['encore_asset_registry']['registered_files'] ?? [], 126 | $this->assetRegistry->getRegisteredFiles() 127 | ); 128 | } 129 | 130 | private function collectDefaultAttributes(): array 131 | { 132 | return array_replace( 133 | $this->controller->config['encore_asset_registry']['default_attributes'] ?? [], 134 | $this->assetRegistry->getDefaultAttributes() 135 | ); 136 | } 137 | 138 | private function getBooleanConfigByPath(string $path): bool 139 | { 140 | if ($this->settingsService->getSettings() !== []) { 141 | return $this->settingsService->getBooleanByPath($path); 142 | } 143 | 144 | $cachedSettings = $this->controller->config['encore_asset_registry']['settings'] ?? []; 145 | 146 | return (bool) ObjectAccess::getPropertyPath($cachedSettings, $path); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/SvgViewHelper.php: -------------------------------------------------------------------------------- 1 | registerTagAttribute('class', 'string', 'CSS class(es) for this element'); 48 | $this->registerTagAttribute('id', 'string', 'Unique (in this file) identifier for this HTML element.'); 49 | $this->registerTagAttribute( 50 | 'lang', 51 | 'string', 52 | 'Language for this element. Use short names specified in RFC 1766' 53 | ); 54 | $this->registerTagAttribute('style', 'string', 'Individual CSS styles for this element'); 55 | $this->registerTagAttribute('accesskey', 'string', 'Keyboard shortcut to access this element'); 56 | $this->registerTagAttribute('tabindex', 'integer', 'Specifies the tab order of this element'); 57 | $this->registerTagAttribute('onclick', 'string', 'JavaScript evaluated for the onclick event'); 58 | 59 | $this->registerArgument('title', 'string', 'Title', false); 60 | $this->registerArgument('description', 'string', 'Description', false); 61 | $this->registerArgument('src', 'string', 'Path to the svg file', true); 62 | $this->registerArgument('role', 'string', 'Role', false, 'img'); 63 | $this->registerArgument('name', 'string', 'The icon name of the sprite', true); 64 | $this->registerArgument('inline', 'string', 'Inline icon instead of referencing it', false, false); 65 | $this->registerArgument('width', 'string', 'Width of the image.'); 66 | $this->registerArgument('height', 'string', 'Height of the image.'); 67 | $this->registerArgument('absolute', 'bool', 'Force absolute URL', false, false); 68 | } 69 | 70 | public function render(): string 71 | { 72 | try { 73 | $image = $this->imageService->getImage($this->arguments['src'], null, false); 74 | $imageUri = $this->imageService->getImageUri($image, (bool) $this->arguments['absolute']); 75 | $imageUri = GeneralUtility::createVersionNumberedFilename($imageUri); 76 | $imageContents = $image->getContents(); 77 | } catch (FolderDoesNotExistException) { 78 | $imageUri = $this->arguments['src']; 79 | $imageContents = $this->filesystem->get($imageUri); 80 | } 81 | 82 | $content = []; 83 | $uniqueId = 'unique'; 84 | $ariaLabelledBy = []; 85 | 86 | if ($this->arguments['title'] || $this->arguments['description']) { 87 | $uniqueId = $this->idGenerator->generate(); 88 | } 89 | 90 | if ($this->arguments['title']) { 91 | $titleId = sprintf('title-%s', $uniqueId); 92 | $ariaLabelledBy[] = $titleId; 93 | $content[] = sprintf( 94 | '
120 | ```
121 |
122 | This way of using the AssetViewHelper is similar to the `asset` function used in Twig templates with Symfony.
123 |
124 | ## Additional
125 |
126 | 1. If you are in production mode and set enableVersioning(true) then you should set the option
127 |
128 | ```php
129 | $GLOBALS['TYPO3_CONF_VARS']['FE']['versionNumberInFilename'] = ''
130 | ```
131 |
132 | 2. Defining Multiple Webpack Configurations ([see](https://symfony.com/doc/current/frontend/encore/advanced-config.html#defining-multiple-webpack-configurations))
133 |
134 | Then you have to define your builds in your TypoScript-Setup:
135 |
136 | ```php
137 | plugin.tx_typo3encore {
138 | settings {
139 | builds {
140 | firstBuild = EXT:typo3_encore/Resources/Public/FirstBuild
141 | secondBuild = EXT:typo3_encore/Resources/Public/SecondBuild
142 | }
143 | }
144 | }
145 | ```
146 |
147 | Finally, you can specify which build to use:
148 |
149 | ```php
150 | page.includeCSS {
151 | # Pattern typo3_encore:buildName:entryName
152 | app = typo3_encore:firstBuild:app
153 | }
154 | ```
155 |
156 | ```html
157 | {namespace encore = Ssch\Typo3Encore\ViewHelpers}
158 |
159 |