├── 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 | '%s', 95 | $titleId, 96 | htmlspecialchars((string) $this->arguments['title'], ENT_QUOTES | ENT_HTML5) 97 | ); 98 | } 99 | 100 | if ($this->arguments['description']) { 101 | $descriptionId = sprintf('description-%s', $uniqueId); 102 | $ariaLabelledBy[] = $descriptionId; 103 | $content[] = sprintf( 104 | '%s', 105 | $descriptionId, 106 | htmlspecialchars((string) $this->arguments['description'], ENT_QUOTES | ENT_HTML5) 107 | ); 108 | } 109 | 110 | if (count($ariaLabelledBy) > 0) { 111 | $this->tag->addAttribute('aria-labelledby', implode(' ', $ariaLabelledBy)); 112 | } 113 | 114 | $name = (string) $this->arguments['name']; 115 | if ($this->arguments['inline']) { 116 | $doc = new DOMDocument(); 117 | $doc->loadXML($imageContents); 118 | $xpath = new DOMXPath($doc); 119 | $iconNodeList = $xpath->query("//*[@id='{$name}']"); 120 | 121 | if (! $iconNodeList instanceof DOMNodeList) { 122 | throw new UnexpectedValueException('Could not query for iconNodeList'); 123 | } 124 | 125 | $icon = $iconNodeList 126 | ->item(0); 127 | 128 | if ($icon !== null) { 129 | if ($icon instanceof DOMElement && $icon->hasAttribute('viewBox')) { 130 | $this->tag->addAttribute('viewBox', $icon->getAttribute('viewBox')); 131 | } 132 | foreach ($icon->childNodes as $node) { 133 | if ($node->ownerDocument === null) { 134 | continue; 135 | } 136 | $content[] = $node->ownerDocument->saveXML($node); 137 | } 138 | } 139 | } else { 140 | $content[] = sprintf( 141 | '', 142 | $imageUri, 143 | htmlspecialchars($name, ENT_QUOTES | ENT_HTML5) 144 | ); 145 | } 146 | 147 | $this->tag->setContent(implode('', $content)); 148 | 149 | if ($this->arguments['width']) { 150 | $this->tag->addAttribute('width', $this->arguments['width']); 151 | } 152 | 153 | if ($this->arguments['height']) { 154 | $this->tag->addAttribute('height', $this->arguments['height']); 155 | } 156 | 157 | $this->tag->addAttribute('xmlns', 'http://www.w3.org/2000/svg'); 158 | $this->tag->addAttribute('focusable', 'false'); 159 | 160 | if ($this->arguments['role']) { 161 | $this->tag->addAttribute('role', $this->arguments['role']); 162 | } 163 | 164 | return $this->tag->render(); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/Stimulus/AbstractViewHelper.php: -------------------------------------------------------------------------------- 1 | $values) { 51 | $dto->addController($controller, $values); 52 | } 53 | 54 | return $dto; 55 | } 56 | 57 | $dto->addController($controllerName, $controllerValues, $controllerClasses); 58 | 59 | return $dto; 60 | } 61 | 62 | /** 63 | * @param string|array $controllerName the Stimulus controller name 64 | * @param array $parameters Parameters to pass to the action. Optional. 65 | */ 66 | public function renderStimulusAction( 67 | $controllerName, 68 | ?string $actionName = null, 69 | ?string $eventName = null, 70 | array $parameters = [] 71 | ): StimulusActionsDto { 72 | $dto = new StimulusActionsDto(); 73 | 74 | if (\is_array($controllerName)) { 75 | trigger_deprecation( 76 | 'symfony/webpack-encore-bundle', 77 | 'v1.15.0', 78 | 'Passing an array as first argument of stimulus_action() is deprecated.', 79 | E_USER_DEPRECATED 80 | ); 81 | 82 | if ($actionName !== null || $eventName !== null || $parameters !== []) { 83 | throw new \InvalidArgumentException( 84 | 'You cannot pass a string to the second or third argument nor an array to the fourth argument while passing an array to the first argument of stimulus_action(): check the documentation.' 85 | ); 86 | } 87 | 88 | $data = $controllerName; 89 | 90 | foreach ($data as $controller => $controllerActions) { 91 | if (\is_string($controllerActions)) { 92 | $controllerActions = [[$controllerActions]]; 93 | } 94 | 95 | foreach ($controllerActions as $possibleEventName => $controllerAction) { 96 | if (\is_string($possibleEventName) && \is_string($controllerAction)) { 97 | $controllerAction = [ 98 | $possibleEventName => $controllerAction, 99 | ]; 100 | } elseif (\is_string($controllerAction)) { 101 | $controllerAction = [$controllerAction]; 102 | } 103 | 104 | foreach ($controllerAction as $event => $action) { 105 | $dto->addAction($controller, $action, \is_string($event) ? $event : null); 106 | } 107 | } 108 | } 109 | 110 | return $dto; 111 | } 112 | 113 | $dto->addAction($controllerName, (string) $actionName, $eventName, $parameters); 114 | 115 | return $dto; 116 | } 117 | 118 | public function appendStimulusController( 119 | StimulusControllersDto $dto, 120 | string $controllerName, 121 | array $controllerValues = [], 122 | array $controllerClasses = [] 123 | ): StimulusControllersDto { 124 | $dto->addController($controllerName, $controllerValues, $controllerClasses); 125 | 126 | return $dto; 127 | } 128 | 129 | /** 130 | * @param array $parameters Parameters to pass to the action. Optional. 131 | */ 132 | public function appendStimulusAction( 133 | StimulusActionsDto $dto, 134 | string $controllerName, 135 | string $actionName, 136 | ?string $eventName = null, 137 | array $parameters = [] 138 | ): StimulusActionsDto { 139 | $dto->addAction($controllerName, $actionName, $eventName, $parameters); 140 | 141 | return $dto; 142 | } 143 | 144 | /** 145 | * @param string|array $controllerName the Stimulus controller name 146 | * @param string|null $targetNames The space-separated list of target names if a string is passed to the 1st argument. Optional. 147 | */ 148 | public function renderStimulusTarget($controllerName, ?string $targetNames = null): StimulusTargetsDto 149 | { 150 | $dto = new StimulusTargetsDto(); 151 | if (\is_array($controllerName)) { 152 | trigger_deprecation( 153 | 'symfony/webpack-encore-bundle', 154 | 'v1.15.0', 155 | 'Passing an array as first argument of stimulus_target() is deprecated.', 156 | E_USER_DEPRECATED 157 | ); 158 | 159 | if ($targetNames !== null) { 160 | throw new \InvalidArgumentException( 161 | 'You cannot pass a string to the second argument while passing an array to the first argument of stimulus_target(): check the documentation.' 162 | ); 163 | } 164 | 165 | $data = $controllerName; 166 | 167 | foreach ($data as $controller => $targets) { 168 | $dto->addTarget($controller, $targets); 169 | } 170 | 171 | return $dto; 172 | } 173 | 174 | $dto->addTarget($controllerName, $targetNames); 175 | 176 | return $dto; 177 | } 178 | 179 | /** 180 | * @param string $controllerName the Stimulus controller name 181 | * @param string|null $targetNames The space-separated list of target names if a string is passed to the 1st argument. Optional. 182 | */ 183 | public function appendStimulusTarget( 184 | StimulusTargetsDto $dto, 185 | string $controllerName, 186 | ?string $targetNames = null 187 | ): StimulusTargetsDto { 188 | $dto->addTarget($controllerName, $targetNames); 189 | 190 | return $dto; 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /Classes/Asset/TagRenderer.php: -------------------------------------------------------------------------------- 1 | applicationType = array_key_exists( 36 | 'TYPO3_REQUEST', 37 | $GLOBALS 38 | ) && $GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface ? ApplicationType::fromRequest( 39 | $GLOBALS['TYPO3_REQUEST'] 40 | ) : null; 41 | } catch (RuntimeException) { 42 | $this->applicationType = null; 43 | } 44 | } 45 | 46 | public function renderWebpackScriptTags(ScriptTag $scriptTag): void 47 | { 48 | $parameters = $scriptTag->getParameters(); 49 | 50 | /** @var PageRenderer $pageRenderer */ 51 | $pageRenderer = $scriptTag->getPageRenderer() ?? GeneralUtility::makeInstance(PageRenderer::class); 52 | $entryPointLookup = $this->getEntrypointLookup($scriptTag->getBuildName()); 53 | 54 | $integrityHashes = ($entryPointLookup instanceof IntegrityDataProviderInterface) ? $entryPointLookup->getIntegrityData() : []; 55 | $files = $entryPointLookup->getJavaScriptFiles($scriptTag->getEntryName()); 56 | 57 | unset($parameters['file']); 58 | 59 | $wrapFirst = ''; 60 | $wrapLast = ''; 61 | $fileCount = count($files); 62 | if (! empty($parameters['allWrap']) && $fileCount > 1) { 63 | // If there are multiple files, allWrap should wrap all. 64 | // To do this, it's split up into two parts. The first part wraps the first file 65 | // and the second part wraps the last file. 66 | $splitChar = ! empty($parameters['splitChar']) ? $parameters['splitChar'] : '|'; 67 | $wrapArr = explode($splitChar, (string) $parameters['allWrap'], 2); 68 | $wrapFirst = $wrapArr[0] . $splitChar; 69 | $wrapLast = $splitChar . $wrapArr[1]; 70 | unset($parameters['allWrap']); 71 | } 72 | 73 | // We do not want to replace null values in $attributes 74 | $parameters = array_filter($parameters, static fn ($param) => $param !== null); 75 | 76 | foreach ($files as $index => $file) { 77 | $this->addAdditionalAbsRefPrefixDirectories($file); 78 | 79 | $allWrap = ''; 80 | if (! $index) { 81 | // first file 82 | $allWrap = $wrapFirst; 83 | } elseif ($index === $fileCount - 1) { 84 | // last file 85 | $allWrap = $wrapLast; 86 | } 87 | 88 | $attributes = array_replace([ 89 | 'file' => $this->removeLeadingSlash($file, $parameters) ? ltrim((string) $file, '/') : $file, 90 | 'type' => $this->removeType($parameters) ? '' : 'text/javascript', 91 | 'compress' => false, 92 | 'forceOnTop' => false, 93 | 'allWrap' => $allWrap, 94 | 'excludeFromConcatenation' => true, 95 | 'splitChar' => '|', 96 | 'async' => false, 97 | 'integrity' => $integrityHashes[$file] ?? '', 98 | 'defer' => false, 99 | 'crossorigin' => '', 100 | ], $parameters); 101 | 102 | $attributes = array_values($attributes); 103 | 104 | $pageRendererMethodName = 'addJS' . ($scriptTag->getPosition() === self::POSITION_FOOTER ? 'Footer' : ''); 105 | 106 | if ($scriptTag->isLibrary()) { 107 | $pageRendererMethodName .= 'Library'; 108 | $filename = basename((string) $file); 109 | $pageRenderer->{$pageRendererMethodName}($filename, ...$attributes); 110 | } else { 111 | $pageRendererMethodName .= 'File'; 112 | $pageRenderer->{$pageRendererMethodName}(...$attributes); 113 | } 114 | 115 | if ($scriptTag->isRegisterFile() === true) { 116 | $this->assetRegistry->registerFile(new File($file, FileType::createScript(), [ 117 | 'integrity' => $integrityHashes[$file] ?? false, 118 | ])); 119 | } 120 | } 121 | } 122 | 123 | public function renderWebpackLinkTags(LinkTag $linkTag): void 124 | { 125 | $parameters = $linkTag->getParameters(); 126 | 127 | /** @var PageRenderer $pageRenderer */ 128 | $pageRenderer = $linkTag->getPageRenderer() ?? GeneralUtility::makeInstance(PageRenderer::class); 129 | $entryPointLookup = $this->getEntrypointLookup($linkTag->getBuildName()); 130 | $files = $entryPointLookup->getCssFiles($linkTag->getEntryName()); 131 | 132 | unset($parameters['file']); 133 | 134 | $wrapFirst = ''; 135 | $wrapLast = ''; 136 | $fileCount = count($files); 137 | if (! empty($parameters['allWrap']) && $fileCount > 1) { 138 | // If there are multiple files, allWrap should wrap all. 139 | // To do this, it's split up into two parts. The first part wraps the first file 140 | // and the second part wraps the last file. 141 | $splitChar = ! empty($parameters['splitChar']) ? $parameters['splitChar'] : '|'; 142 | $wrapArr = explode($splitChar, (string) $parameters['allWrap'], 2); 143 | $wrapFirst = $wrapArr[0] . $splitChar; 144 | $wrapLast = $splitChar . $wrapArr[1]; 145 | unset($parameters['allWrap']); 146 | } 147 | 148 | foreach ($files as $index => $file) { 149 | $this->addAdditionalAbsRefPrefixDirectories($file); 150 | 151 | $allWrap = ''; 152 | if (! $index) { 153 | // first file 154 | $allWrap = $wrapFirst; 155 | } elseif ($index === $fileCount - 1) { 156 | // last file 157 | $allWrap = $wrapLast; 158 | } 159 | 160 | $attributes = array_replace([ 161 | 'file' => $this->removeLeadingSlash($file, $parameters) ? ltrim((string) $file, '/') : $file, 162 | 'rel' => 'stylesheet', 163 | 'media' => $linkTag->getMedia(), 164 | 'title' => '', 165 | 'compress' => false, 166 | 'forceOnTop' => false, 167 | 'allWrap' => $allWrap, 168 | 'excludeFromConcatenation' => true, 169 | 'splitChar' => '|', 170 | 'inline' => false, 171 | ], $parameters); 172 | 173 | $attributes = array_values($attributes); 174 | 175 | $pageRenderer->addCssFile(...$attributes); 176 | 177 | if ($linkTag->isRegisterFile() === true) { 178 | $this->assetRegistry->registerFile(new File($file, FileType::createStyle())); 179 | } 180 | } 181 | } 182 | 183 | private function getEntrypointLookup(string $buildName): EntrypointLookupInterface 184 | { 185 | return $this->entrypointLookupCollection->getEntrypointLookup($buildName); 186 | } 187 | 188 | private function addAdditionalAbsRefPrefixDirectories(string $file): void 189 | { 190 | if (isset($GLOBALS['TYPO3_CONF_VARS']['FE']['additionalAbsRefPrefixDirectories']) && 191 | is_string($GLOBALS['TYPO3_CONF_VARS']['FE']['additionalAbsRefPrefixDirectories']) 192 | ) { 193 | $directories = GeneralUtility::trimExplode( 194 | ',', 195 | $GLOBALS['TYPO3_CONF_VARS']['FE']['additionalAbsRefPrefixDirectories'], 196 | true 197 | ); 198 | 199 | $newDir = basename(dirname($file)) . '/'; 200 | 201 | if (in_array($newDir, $directories, true) === false) { 202 | $GLOBALS['TYPO3_CONF_VARS']['FE']['additionalAbsRefPrefixDirectories'] .= ',' . $newDir; 203 | } 204 | } 205 | } 206 | 207 | private function removeLeadingSlash(string $file, array $parameters): bool 208 | { 209 | if (array_key_exists('inline', $parameters) && (bool) $parameters['inline']) { 210 | return true; 211 | } 212 | 213 | if ($this->applicationType === null) { 214 | return false; 215 | } 216 | 217 | if (! $this->applicationType->isFrontend()) { 218 | return false; 219 | } 220 | 221 | if ($this->getTypoScriptFrontendController()->absRefPrefix === '') { 222 | return false; 223 | } 224 | 225 | if ($this->getTypoScriptFrontendController()->absRefPrefix === '/') { 226 | return true; 227 | } 228 | 229 | if (str_starts_with($file, $this->getTypoScriptFrontendController()->absRefPrefix)) { 230 | return false; 231 | } 232 | 233 | return ! GeneralUtility::isValidUrl($file); 234 | } 235 | 236 | private function getTypoScriptFrontendController(): TypoScriptFrontendController 237 | { 238 | return $GLOBALS['TSFE']; 239 | } 240 | 241 | private function removeType(array $parameters): bool 242 | { 243 | if (array_key_exists('type', $parameters)) { 244 | return false; 245 | } 246 | 247 | if ($this->applicationType === null) { 248 | return false; 249 | } 250 | 251 | if (! $this->applicationType->isFrontend()) { 252 | return false; 253 | } 254 | 255 | if (! isset($this->getTypoScriptFrontendController()->config['config']['doctype']) || $this->getTypoScriptFrontendController()->config['config']['doctype'] !== 'html5') { 256 | return false; 257 | } 258 | 259 | return true; 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Downloads](https://img.shields.io/packagist/dt/ssch/typo3-encore.svg?style=flat-square)](https://packagist.org/packages/ssch/typo3-encore) 2 | 3 | TYPO3 integration with Webpack Encore! 4 | ====================================== 5 | 6 | This extension allows you to use the `splitEntryChunks()` feature 7 | from [Webpack Encore](https://symfony.com/doc/current/frontend.html) 8 | by reading an `entrypoints.json` file and helping you render all of 9 | the dynamic `script` and `link` tags needed. 10 | 11 | ``` 12 | composer require ssch/typo3-encore 13 | ``` 14 | 15 | ## How to use 16 | 17 | 1. First of all install Webpack Encore as stated in the [documentation](https://symfony.com/doc/current/frontend.html). 18 | You should really be able to use all of the things described in the documentation. 19 | Like Sass-Loader, Vue-Loader etc. These things are completely independent from this little extension. 20 | 21 | You can also use the enableVersioning() of files (mostly used only in production context). 22 | You can also use the enableIntegrityHashes(). This is taking into account if the files are included. 23 | 24 | 2. Define your entry path(s) and the output path (usually your Resource/Public/ folder in your Package extension) in the webpack.config.js 25 | 26 | 3. Afterwards set the two TypoScript constants to point to the manifest.json and the entrypoints.json located in the configured output folder 27 | ```php 28 | plugin.tx_typo3encore { 29 | settings { 30 | entrypointJsonPath = EXT:typo3_encore/Resources/Public/entrypoints.json 31 | manifestJsonPath = EXT:typo3_encore/Resources/Public/manifest.json 32 | } 33 | } 34 | ``` 35 | 36 | 4. In your Page templates/layout you can then use the ViewHelpers to integrate the CSS- and JS-Files in your website 37 | ```html 38 | {namespace encore = Ssch\Typo3Encore\ViewHelpers} 39 | 40 | 41 | 42 | ``` 43 | 44 | If you have defined multiple entries you can define the desired entryName in the ViewHelpers 45 | ```html 46 | {namespace encore = Ssch\Typo3Encore\ViewHelpers} 47 | 48 | 49 | 50 | ``` 51 | 52 | 53 | Alternatively you can also include the files via TypoScript 54 | 55 | ```php 56 | page.includeCSS { 57 | # Pattern typo3_encore:entryName 58 | app = typo3_encore:app 59 | # If you want to ensure that this file is loaded first uncomment the next line 60 | # app.forceOnTop = 1 61 | } 62 | 63 | page.includeJS { 64 | # Pattern typo3_encore:entryName 65 | app = typo3_encore:app 66 | # If you want to ensure that this file is loaded first uncomment the next line 67 | # app.forceOnTop = 1 68 | } 69 | 70 | page.includeJSFooter { 71 | # Pattern typo3_encore:entryName 72 | app = typo3_encore:app 73 | } 74 | ``` 75 | 76 | Note the prefix typo3_encore: This is important in order to render the files correctly. 77 | You can then use all other known settings to include your files. 78 | 79 | You don´t have to care about including it only once. This will not happen during one request cycle unless you want to. 80 | 81 | It is also possible to use the inclusion via the prefix typo3_encore in backend specific contexts. For example like so: 82 | 83 | ```html 84 | 85 | 86 | 87 | ``` 88 | 89 | ### HTTP/2 Preloading 90 | 91 | All css and javascript files managed by the extension will be added to the AssetRegistry class during rendering. 92 | For these assets a Link HTTP header is created, which are the key to optimize the application performance when using HTTP/2 and preloading capabilities of modern web browsers. 93 | 94 | Technically this is done by a PSR-15 Middleware. 95 | 96 | If you want to add additional files to the AssetRegistry you can use the PreloadViewHelper: 97 | 98 | ```html 99 | {namespace encore = Ssch\Typo3Encore\ViewHelpers} 100 | 101 | 102 | ``` 103 | 104 | Watch out, the example also uses the AssetViewHelper. The AssetViewHelper behind the scenes makes a look up to the manifest.json file. 105 | So you can also leverage the versioning feature provided by Webpack. 106 | 107 | ### Static assets 108 | 109 | Sometimes you might need to reference static assets (like image files) directly in your fluid templates. You can use the Encore `copyFiles` function to instruct Webpack to copy static assets to your output folder (see https://symfony.com/doc/current/frontend/encore/copy-files.html#referencing-image-files-from-a-template). 110 | 111 | To reference a static asset file from a fluid template, you can then use the AssetViewHelper to get the file path (including the hash if versioning is enabled). 112 | 113 | Note that the AssetViewHelper does not render anything but just returns the path to the file, so you will probably use inline notation to, e.g., display an image: 114 | 115 | ``` 116 | {namespace encore = Ssch\Typo3Encore\ViewHelpers} 117 | 118 | 119 | My image 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 | 160 | ``` 161 | 162 | 3. **Important note on TYPO3 JS/CSS concatenation and compression while working with encore dev-server mode** 163 | 164 | If you are using the Encore dev server (`encore dev-server`), ensure that TYPO3's asset concatenation and compression 165 | are disabled. 166 | 167 | Enabling these options (`config.concatenateCss = 1`, `config.concatenateJs = 1`, `config.compressCss = 1`, or `config.compressJs = 1`) 168 | will prevent the dev server's hot module replacement (HMR) from working correctly. Additionally, TYPO3 may produce 169 | a large volume of temporary files (potentially gigabytes), significantly slowing down Docker-based development environments. 170 | 171 | See [Issue #234](https://github.com/sabbelasichon/typo3_encore/issues/234) for more details. 172 | 173 | Example TypoScript to disable concatenation/compression in Development: 174 | ```typoscript 175 | [like(applicationContext, "Development*")] 176 | config { 177 | compressCss = 0 178 | concatenateCss = 0 179 | compressJs = 0 180 | concatenateJs = 0 181 | } 182 | [global] 183 | ``` 184 | 185 | ### CSS for CKEditor 186 | 187 | It is possible to configure encore so that you can use a CSS file for the CKEditor which is generated by webpack and even works with enabled versioning. 188 | Two steps are required to do so: 189 | 190 | 1. Define an entrypoint for the RTE in your `webpack.config.js`, e.g. 191 | ```js 192 | Encore 193 | .addStyleEntry('rte', './assets/rte.scss') 194 | ``` 195 | 196 | 2. Add in the CKEditor yaml configuration 197 | 198 | ```yaml 199 | editor: 200 | config: 201 | contentsCss: "typo3_encore:rte" 202 | ``` 203 | 204 | ## Getting Started with Webpack Encore 205 | 206 | Although the documentation of Webpack Encore is awesome, i am going to provide a minimalistic how to install the frontend related things. 207 | I assume some basic knowledge of modern frontend development. 208 | 209 | ### Install Encore into your project via Yarn or Npm: 210 | First, make sure you install [Node.js](https://nodejs.org/en/download/) and also the [Yarn](https://yarnpkg.com/lang/en/docs/install/) or [npm](https://www.npmjs.com/get-npm) package manager. 211 | 212 | 213 | ```cli 214 | yarn add @symfony/webpack-encore --dev 215 | ``` 216 | 217 | This command creates or modifies a package.json file and downloads dependencies into a node_modules/ directory. 218 | Yarn also creates/updates a yarn.lock (called package-lock.json if you use npm). 219 | 220 | You should commit package.json and yarn.lock (or package-lock.json if using npm) to version control, but ignore the node_modules/ folder. 221 | 222 | ### Creating the webpack.config.js File 223 | Next, we are going to create a webpack.config.js file at the root of our project. 224 | This is the main config file for both Webpack and Webpack Encore: 225 | 226 | ```javascript 227 | var Encore = require('@symfony/webpack-encore'); 228 | 229 | Encore 230 | // the directory where compiled assets will be stored 231 | .setOutputPath('public/typo3conf/ext/my_sitepackage/Resources/Public/') 232 | 233 | // public path used by the web server to access the output path 234 | .setPublicPath('/typo3conf/ext/my_sitepackage/Resources/Public/') 235 | 236 | // only needed for CDN's or sub-directory deploy 237 | // .setManifestKeyPrefix('build/') 238 | 239 | // Copy some static images to your -> https://symfony.com/doc/current/frontend/encore/copy-files.html 240 | .copyFiles({ 241 | from: './src/images', 242 | // Optional target path, relative to the output dir 243 | to: 'images/[path][name].[ext]', 244 | includeSubdirectories: false, 245 | // if versioning is enabled, add the file hash too 246 | to: 'images/[path][name].[hash:8].[ext]', 247 | // only copy files matching this pattern 248 | pattern: /\.(png|jpg|jpeg)$/ 249 | }) 250 | 251 | /* 252 | * ENTRY CONFIG 253 | * 254 | * Add 1 entry for each "page" of your app 255 | * (including one that's included on every page - e.g. "app") 256 | * 257 | * Each entry will result in one JavaScript file (e.g. app.js) 258 | * and one CSS file (e.g. app.css) if you JavaScript imports CSS. 259 | */ 260 | .addEntry('app', './src/js/app.js') 261 | .addEntry('homepage', './src/js/homepage.js') 262 | 263 | // will require an extra script tag for runtime.js 264 | // but, you probably want this, unless you're building a single-page app 265 | .enableSingleRuntimeChunk() 266 | 267 | .cleanupOutputBeforeBuild() 268 | .enableSourceMaps(!Encore.isProduction()) 269 | 270 | // enables hashed filenames (e.g. app.abc123.css) 271 | .enableVersioning(Encore.isProduction()) 272 | 273 | // uncomment if you use TypeScript -> https://symfony.com/doc/current/frontend/encore/typescript.html 274 | // .enableTypeScriptLoader() 275 | 276 | // uncomment if you are using Sass/SCSS files -> https://symfony.com/doc/current/frontend/encore/css-preprocessors.html 277 | // .enableSassLoader() 278 | 279 | // uncomment if you're having problems with a jQuery plugin -> https://symfony.com/doc/current/frontend/encore/legacy-applications.html 280 | // .autoProvidejQuery() 281 | 282 | // uncomment if you use the postcss -> https://symfony.com/doc/current/frontend/encore/postcss.html 283 | // .enablePostCssLoader() 284 | 285 | 286 | // uncomment if you want to use vue -> https://symfony.com/doc/current/frontend/encore/vuejs.html 287 | // .enableVueLoader() 288 | 289 | // uncomment if you´re want to lint your sources 290 | // .enableEslintLoader() 291 | 292 | // uncomment if you´re want to have integrity hashes for your script tags, the extension takes care of it 293 | // .enableIntegrityHashes() 294 | 295 | // uncomment if you´re want to share general code for the different entries -> https://symfony.com/doc/current/frontend/encore/split-chunks.html 296 | // .splitEntryChunks() 297 | ; 298 | 299 | // Uncomment if you are going to use a CDN -> https://symfony.com/doc/current/frontend/encore/cdn.html 300 | // if (Encore.isProduction()) { 301 | //Encore.setPublicPath('https://my-cool-app.com.global.prod.fastly.net'); 302 | 303 | // guarantee that the keys in manifest.json are *still* 304 | // prefixed with build/ 305 | // (e.g. "build/dashboard.js": "https://my-cool-app.com.global.prod.fastly.net/dashboard.js") 306 | // Encore.setManifestKeyPrefix('build/'); 307 | // } 308 | 309 | module.exports = Encore.getWebpackConfig(); 310 | ``` 311 | 312 | ### Working with Watcher & TYPO3 with Content Security Policy 313 | 314 | If you encounter problems with Content Security Policy and watcher add the following to your additional.php 315 | 316 | ```php 317 | // disable CSP if "npm run watch" is used 318 | if (strpos(file_get_contents(\TYPO3\CMS\Core\Core\Environment::getPublicPath() . '/build/entrypoints.json'), 'build/vendors-node_modules') !== false) { 319 | $GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['security.frontend.enforceContentSecurityPolicy'] = false; 320 | } 321 | ``` 322 | 323 | ### Working with typo3/cms-composer-installers 4+ or TYPO3 12 324 | 325 | The `typo3/cms-composer-installers` library takes care of moving TYPO3-specific assets in the right place 326 | in Composer-based installations, like copying extensions to `typo3conf/ext`. Starting with version 4.0 327 | (currently at RC1 stage), the extensions will remain in `vendor/vendor_name`. The `Resources/Public` 328 | directory of each extension is symlinked from the `public/assets` directory using hashes. While it is 329 | possible to target the symlink, the fact that it is a hash makes it a bit flaky. 330 | 331 | The recommendation is to use another build directory, not located inside an extension. 332 | As an example, asuming that you use `public/build`, the configuration in `webpack.config.js` 333 | would be modified as follows: 334 | 335 | ```javascript 336 | var Encore = require('@symfony/webpack-encore'); 337 | 338 | Encore 339 | .setOutputPath('../../public/build') 340 | .setPublicPath('/build') 341 | ... 342 | ``` 343 | 344 | The TypoScript constants have to be modified accordingly: 345 | 346 | ```typo3_typoscript 347 | plugin.tx_typo3encore { 348 | settings { 349 | # These paths are relative to the web root (public) directory 350 | entrypointJsonPath = build/entrypoints.json 351 | manifestJsonPath = build/manifest.json 352 | } 353 | } 354 | ``` 355 | 356 | If the site `base` configuration (`config/sites/yoursite/config.yaml`) is a subdirectory/subpath of your domain, it is required to add your new "build" directory as an additional absolute directory. 357 | This can be done in your "Configure Installation-Wide Options" (TYPO3 <= 11: `typo3conf/LocalConfiguration.php`; TYPO3 >= 12: `config/system/settings.php`) 358 | ``` 359 | [FE][additionalAbsRefPrefixDirectories]: build 360 | ``` 361 | 362 | ### The realm of Webpack plugins 363 | Encore already ships with a lot of useful plugins for the daily work. 364 | But someday you are gonna get to the point where you need more. 365 | 366 | #### Generating icons and inject them automatically 367 | 368 | Install [webapp-webpack-plugin](https://github.com/brunocodutra/webapp-webpack-plugin) and [html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin). 369 | 370 | ```javascript 371 | const WebappWebpackPlugin = require('webapp-webpack-plugin'); 372 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 373 | 374 | Encore.addPlugin(new HtmlWebpackPlugin( 375 | { 376 | inject: false, 377 | minify: false, 378 | template: 'public/typo3conf/ext/typo3_encore/Resources/Private/Templates/Favicons.html', 379 | filename: 'favicons.html', 380 | } 381 | )) 382 | .addPlugin(new WebappWebpackPlugin({ 383 | inject: htmlPlugin => htmlPlugin.options.filename === 'favicons.html', 384 | logo: './src/images/logo.png', 385 | force: true, 386 | favicons: { 387 | start_url: null, 388 | lang: null, 389 | icons: { 390 | android: true, 391 | appleIcon: true, 392 | appleStartup: true, 393 | windows: true, 394 | yandex: true, 395 | favicons: true, 396 | coast: true, 397 | firefox: true, 398 | opengraph: false, 399 | twitter: false 400 | } 401 | } 402 | })) 403 | ``` 404 | 405 | In order to inject the html file in the header of your TYPO3 just include the template file: 406 | 407 | ```php 408 | page.headerData.2039 = FLUIDTEMPLATE 409 | page.headerData.2039 { 410 | file = EXT:typo3_encore/Resources/Public/favicons.html 411 | } 412 | ``` 413 | 414 | #### Generating a svg sprite 415 | 416 | Install [svg-sprite-loader](https://github.com/kisenka/svg-sprite-loader#installation) 417 | ```javascript 418 | const SpritePlugin = require('svg-sprite-loader/plugin'); 419 | 420 | Encore.addLoader({ 421 | test: /\src\/icons\/.svg$/, 422 | loader: 'svg-sprite-loader', 423 | options: { 424 | extract: true, 425 | } 426 | }).addPlugin(new SpritePlugin()) 427 | 428 | ``` 429 | 430 | Now you have to import all your svg files in your javascript 431 | 432 | ``` 433 | function requireAll(r) { 434 | r.keys().forEach(r); 435 | } 436 | requireAll(require.context('./relative-path-to-svg-folder/svg-sprite/', true, /\.svg$/)); 437 | ``` 438 | 439 | The extension ships with a SvgViewHelper in order to simplify the usage of svg in fluid. 440 | 441 | ```html 442 | {namespace encore = Ssch\Typo3Encore\ViewHelpers} 443 | 444 | 445 | ``` 446 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Some icons used in the TYPO3 project are retrieved from the "Silk" icon set of 2 | Mark James, which can be found at http://famfamfam.com/lab/icons/silk/. This 3 | set is distributed under a Creative Commons Attribution 2.5 License. The 4 | license can be found at http://creativecommons.org/licenses/by/2.5/. 5 | --------------------------------- 6 | 7 | GNU GENERAL PUBLIC LICENSE 8 | Version 2, June 1991 9 | 10 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 11 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 12 | Everyone is permitted to copy and distribute verbatim copies 13 | of this license document, but changing it is not allowed. 14 | 15 | Preamble 16 | 17 | The licenses for most software are designed to take away your 18 | freedom to share and change it. By contrast, the GNU General Public 19 | License is intended to guarantee your freedom to share and change free 20 | software--to make sure the software is free for all its users. This 21 | General Public License applies to most of the Free Software 22 | Foundation's software and to any other program whose authors commit to 23 | using it. (Some other Free Software Foundation software is covered by 24 | the GNU Lesser General Public License instead.) You can apply it to 25 | your programs, too. 26 | 27 | When we speak of free software, we are referring to freedom, not 28 | price. Our General Public Licenses are designed to make sure that you 29 | have the freedom to distribute copies of free software (and charge for 30 | this service if you wish), that you receive source code or can get it 31 | if you want it, that you can change the software or use pieces of it 32 | in new free programs; and that you know you can do these things. 33 | 34 | To protect your rights, we need to make restrictions that forbid 35 | anyone to deny you these rights or to ask you to surrender the rights. 36 | These restrictions translate to certain responsibilities for you if you 37 | distribute copies of the software, or if you modify it. 38 | 39 | For example, if you distribute copies of such a program, whether 40 | gratis or for a fee, you must give the recipients all the rights that 41 | you have. You must make sure that they, too, receive or can get the 42 | source code. And you must show them these terms so they know their 43 | rights. 44 | 45 | We protect your rights with two steps: (1) copyright the software, and 46 | (2) offer you this license which gives you legal permission to copy, 47 | distribute and/or modify the software. 48 | 49 | Also, for each author's protection and ours, we want to make certain 50 | that everyone understands that there is no warranty for this free 51 | software. If the software is modified by someone else and passed on, we 52 | want its recipients to know that what they have is not the original, so 53 | that any problems introduced by others will not reflect on the original 54 | authors' reputations. 55 | 56 | Finally, any free program is threatened constantly by software 57 | patents. We wish to avoid the danger that redistributors of a free 58 | program will individually obtain patent licenses, in effect making the 59 | program proprietary. To prevent this, we have made it clear that any 60 | patent must be licensed for everyone's free use or not licensed at all. 61 | 62 | The precise terms and conditions for copying, distribution and 63 | modification follow. 64 | 65 | GNU GENERAL PUBLIC LICENSE 66 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 67 | 68 | 0. This License applies to any program or other work which contains 69 | a notice placed by the copyright holder saying it may be distributed 70 | under the terms of this General Public License. The "Program", below, 71 | refers to any such program or work, and a "work based on the Program" 72 | means either the Program or any derivative work under copyright law: 73 | that is to say, a work containing the Program or a portion of it, 74 | either verbatim or with modifications and/or translated into another 75 | language. (Hereinafter, translation is included without limitation in 76 | the term "modification".) Each licensee is addressed as "you". 77 | 78 | Activities other than copying, distribution and modification are not 79 | covered by this License; they are outside its scope. The act of 80 | running the Program is not restricted, and the output from the Program 81 | is covered only if its contents constitute a work based on the 82 | Program (independent of having been made by running the Program). 83 | Whether that is true depends on what the Program does. 84 | 85 | 1. You may copy and distribute verbatim copies of the Program's 86 | source code as you receive it, in any medium, provided that you 87 | conspicuously and appropriately publish on each copy an appropriate 88 | copyright notice and disclaimer of warranty; keep intact all the 89 | notices that refer to this License and to the absence of any warranty; 90 | and give any other recipients of the Program a copy of this License 91 | along with the Program. 92 | 93 | You may charge a fee for the physical act of transferring a copy, and 94 | you may at your option offer warranty protection in exchange for a fee. 95 | 96 | 2. You may modify your copy or copies of the Program or any portion 97 | of it, thus forming a work based on the Program, and copy and 98 | distribute such modifications or work under the terms of Section 1 99 | above, provided that you also meet all of these conditions: 100 | 101 | a) You must cause the modified files to carry prominent notices 102 | stating that you changed the files and the date of any change. 103 | 104 | b) You must cause any work that you distribute or publish, that in 105 | whole or in part contains or is derived from the Program or any 106 | part thereof, to be licensed as a whole at no charge to all third 107 | parties under the terms of this License. 108 | 109 | c) If the modified program normally reads commands interactively 110 | when run, you must cause it, when started running for such 111 | interactive use in the most ordinary way, to print or display an 112 | announcement including an appropriate copyright notice and a 113 | notice that there is no warranty (or else, saying that you provide 114 | a warranty) and that users may redistribute the program under 115 | these conditions, and telling the user how to view a copy of this 116 | License. (Exception: if the Program itself is interactive but 117 | does not normally print such an announcement, your work based on 118 | the Program is not required to print an announcement.) 119 | 120 | These requirements apply to the modified work as a whole. If 121 | identifiable sections of that work are not derived from the Program, 122 | and can be reasonably considered independent and separate works in 123 | themselves, then this License, and its terms, do not apply to those 124 | sections when you distribute them as separate works. But when you 125 | distribute the same sections as part of a whole which is a work based 126 | on the Program, the distribution of the whole must be on the terms of 127 | this License, whose permissions for other licensees extend to the 128 | entire whole, and thus to each and every part regardless of who wrote it. 129 | 130 | Thus, it is not the intent of this section to claim rights or contest 131 | your rights to work written entirely by you; rather, the intent is to 132 | exercise the right to control the distribution of derivative or 133 | collective works based on the Program. 134 | 135 | In addition, mere aggregation of another work not based on the Program 136 | with the Program (or with a work based on the Program) on a volume of 137 | a storage or distribution medium does not bring the other work under 138 | the scope of this License. 139 | 140 | 3. You may copy and distribute the Program (or a work based on it, 141 | under Section 2) in object code or executable form under the terms of 142 | Sections 1 and 2 above provided that you also do one of the following: 143 | 144 | a) Accompany it with the complete corresponding machine-readable 145 | source code, which must be distributed under the terms of Sections 146 | 1 and 2 above on a medium customarily used for software interchange; or, 147 | 148 | b) Accompany it with a written offer, valid for at least three 149 | years, to give any third party, for a charge no more than your 150 | cost of physically performing source distribution, a complete 151 | machine-readable copy of the corresponding source code, to be 152 | distributed under the terms of Sections 1 and 2 above on a medium 153 | customarily used for software interchange; or, 154 | 155 | c) Accompany it with the information you received as to the offer 156 | to distribute corresponding source code. (This alternative is 157 | allowed only for noncommercial distribution and only if you 158 | received the program in object code or executable form with such 159 | an offer, in accord with Subsection b above.) 160 | 161 | The source code for a work means the preferred form of the work for 162 | making modifications to it. For an executable work, complete source 163 | code means all the source code for all modules it contains, plus any 164 | associated interface definition files, plus the scripts used to 165 | control compilation and installation of the executable. However, as a 166 | special exception, the source code distributed need not include 167 | anything that is normally distributed (in either source or binary 168 | form) with the major components (compiler, kernel, and so on) of the 169 | operating system on which the executable runs, unless that component 170 | itself accompanies the executable. 171 | 172 | If distribution of executable or object code is made by offering 173 | access to copy from a designated place, then offering equivalent 174 | access to copy the source code from the same place counts as 175 | distribution of the source code, even though third parties are not 176 | compelled to copy the source along with the object code. 177 | 178 | 4. You may not copy, modify, sublicense, or distribute the Program 179 | except as expressly provided under this License. Any attempt 180 | otherwise to copy, modify, sublicense or distribute the Program is 181 | void, and will automatically terminate your rights under this License. 182 | However, parties who have received copies, or rights, from you under 183 | this License will not have their licenses terminated so long as such 184 | parties remain in full compliance. 185 | 186 | 5. You are not required to accept this License, since you have not 187 | signed it. However, nothing else grants you permission to modify or 188 | distribute the Program or its derivative works. These actions are 189 | prohibited by law if you do not accept this License. Therefore, by 190 | modifying or distributing the Program (or any work based on the 191 | Program), you indicate your acceptance of this License to do so, and 192 | all its terms and conditions for copying, distributing or modifying 193 | the Program or works based on it. 194 | 195 | 6. Each time you redistribute the Program (or any work based on the 196 | Program), the recipient automatically receives a license from the 197 | original licensor to copy, distribute or modify the Program subject to 198 | these terms and conditions. You may not impose any further 199 | restrictions on the recipients' exercise of the rights granted herein. 200 | You are not responsible for enforcing compliance by third parties to 201 | this License. 202 | 203 | 7. If, as a consequence of a court judgment or allegation of patent 204 | infringement or for any other reason (not limited to patent issues), 205 | conditions are imposed on you (whether by court order, agreement or 206 | otherwise) that contradict the conditions of this License, they do not 207 | excuse you from the conditions of this License. If you cannot 208 | distribute so as to satisfy simultaneously your obligations under this 209 | License and any other pertinent obligations, then as a consequence you 210 | may not distribute the Program at all. For example, if a patent 211 | license would not permit royalty-free redistribution of the Program by 212 | all those who receive copies directly or indirectly through you, then 213 | the only way you could satisfy both it and this License would be to 214 | refrain entirely from distribution of the Program. 215 | 216 | If any portion of this section is held invalid or unenforceable under 217 | any particular circumstance, the balance of the section is intended to 218 | apply and the section as a whole is intended to apply in other 219 | circumstances. 220 | 221 | It is not the purpose of this section to induce you to infringe any 222 | patents or other property right claims or to contest validity of any 223 | such claims; this section has the sole purpose of protecting the 224 | integrity of the free software distribution system, which is 225 | implemented by public license practices. Many people have made 226 | generous contributions to the wide range of software distributed 227 | through that system in reliance on consistent application of that 228 | system; it is up to the author/donor to decide if he or she is willing 229 | to distribute software through any other system and a licensee cannot 230 | impose that choice. 231 | 232 | This section is intended to make thoroughly clear what is believed to 233 | be a consequence of the rest of this License. 234 | 235 | 8. If the distribution and/or use of the Program is restricted in 236 | certain countries either by patents or by copyrighted interfaces, the 237 | original copyright holder who places the Program under this License 238 | may add an explicit geographical distribution limitation excluding 239 | those countries, so that distribution is permitted only in or among 240 | countries not thus excluded. In such case, this License incorporates 241 | the limitation as if written in the body of this License. 242 | 243 | 9. The Free Software Foundation may publish revised and/or new versions 244 | of the General Public License from time to time. Such new versions will 245 | be similar in spirit to the present version, but may differ in detail to 246 | address new problems or concerns. 247 | 248 | Each version is given a distinguishing version number. If the Program 249 | specifies a version number of this License which applies to it and "any 250 | later version", you have the option of following the terms and conditions 251 | either of that version or of any later version published by the Free 252 | Software Foundation. If the Program does not specify a version number of 253 | this License, you may choose any version ever published by the Free Software 254 | Foundation. 255 | 256 | 10. If you wish to incorporate parts of the Program into other free 257 | programs whose distribution conditions are different, write to the author 258 | to ask for permission. For software which is copyrighted by the Free 259 | Software Foundation, write to the Free Software Foundation; we sometimes 260 | make exceptions for this. Our decision will be guided by the two goals 261 | of preserving the free status of all derivatives of our free software and 262 | of promoting the sharing and reuse of software generally. 263 | 264 | NO WARRANTY 265 | 266 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 267 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 268 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 269 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 270 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 271 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 272 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 273 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 274 | REPAIR OR CORRECTION. 275 | 276 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 277 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 278 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 279 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 280 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 281 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 282 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 283 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 284 | POSSIBILITY OF SUCH DAMAGES. 285 | 286 | END OF TERMS AND CONDITIONS 287 | 288 | How to Apply These Terms to Your New Programs 289 | 290 | If you develop a new program, and you want it to be of the greatest 291 | possible use to the public, the best way to achieve this is to make it 292 | free software which everyone can redistribute and change under these terms. 293 | 294 | To do so, attach the following notices to the program. It is safest 295 | to attach them to the start of each source file to most effectively 296 | convey the exclusion of warranty; and each file should have at least 297 | the "copyright" line and a pointer to where the full notice is found. 298 | 299 | 300 | Copyright (C) 301 | 302 | This program is free software; you can redistribute it and/or modify 303 | it under the terms of the GNU General Public License as published by 304 | the Free Software Foundation; either version 2 of the License, or 305 | (at your option) any later version. 306 | 307 | This program is distributed in the hope that it will be useful, 308 | but WITHOUT ANY WARRANTY; without even the implied warranty of 309 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 310 | GNU General Public License for more details. 311 | 312 | You should have received a copy of the GNU General Public License along 313 | with this program; if not, write to the Free Software Foundation, Inc., 314 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 315 | 316 | Also add information on how to contact you by electronic and paper mail. 317 | 318 | If the program is interactive, make it output a short notice like this 319 | when it starts in an interactive mode: 320 | 321 | Gnomovision version 69, Copyright (C) year name of author 322 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 323 | This is free software, and you are welcome to redistribute it 324 | under certain conditions; type `show c' for details. 325 | 326 | The hypothetical commands `show w' and `show c' should show the appropriate 327 | parts of the General Public License. Of course, the commands you use may 328 | be called something other than `show w' and `show c'; they could even be 329 | mouse-clicks or menu items--whatever suits your program. 330 | 331 | You should also get your employer (if you work as a programmer) or your 332 | school, if any, to sign a "copyright disclaimer" for the program, if 333 | necessary. Here is a sample; alter the names: 334 | 335 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 336 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 337 | 338 | , 1 April 1989 339 | Ty Coon, President of Vice 340 | 341 | This General Public License does not permit incorporating your program into 342 | proprietary programs. If your program is a subroutine library, you may 343 | consider it more useful to permit linking proprietary applications with the 344 | library. If this is what you want to do, use the GNU Lesser General 345 | Public License instead of this License. 346 | --------------------------------------------------------------------------------