Skip to content

Commit

Permalink
feature #51847 [AssetMapper] Allowing for files to be written to some…
Browse files Browse the repository at this point in the history
… non-local location (weaverryan)

This PR was squashed before being merged into the 6.4 branch.

Discussion
----------

[AssetMapper] Allowing for files to be written to some non-local location

| Q             | A
| ------------- | ---
| Branch?       | 6.4
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | Fix #50221
| License       | MIT

Hi!

An attempt at making AssetMapper flexible enough to support non-local filesystems. The "hook point" would be that you could replace the `asset_mapper.local_public_assets_filesystem` service with your own that implements `PublicAssetsFilesystemInterface`. I'm not worried about making the hook point *super* user-friendly: I just want the system to support this now, as trying to add this later (when we need to protect BC) will be harder.

Cheers!

Commits
-------

48a2d68 [AssetMapper] Allowing for files to be written to some non-local location
  • Loading branch information
fabpot committed Oct 20, 2023
2 parents 3b02e73 + 48a2d68 commit d261eeb
Show file tree
Hide file tree
Showing 19 changed files with 347 additions and 166 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1353,13 +1353,17 @@ private function registerAssetMapperConfiguration(array $config, ContainerBuilde
->setArgument(0, $paths)
->setArgument(2, $excludedPathPatterns);

$publicDirName = $this->getPublicDirectoryName($container);
$container->getDefinition('asset_mapper.public_assets_path_resolver')
->setArgument(1, $config['public_prefix'])
->setArgument(2, $publicDirName);
->setArgument(0, $config['public_prefix']);

$container->getDefinition('asset_mapper.command.compile')
->setArgument(5, $publicDirName);
$publicDirectory = $this->getPublicDirectory($container);
$publicAssetsDirectory = rtrim($publicDirectory.'/'.ltrim($config['public_prefix'], '/'), '/');
$container->getDefinition('asset_mapper.local_public_assets_filesystem')
->setArgument(0, $publicDirectory)
;

$container->getDefinition('asset_mapper.compiled_asset_mapper_config_reader')
->setArgument(0, $publicAssetsDirectory);

if (!$config['server']) {
$container->removeDefinition('asset_mapper.dev_server_subscriber');
Expand Down Expand Up @@ -3163,11 +3167,12 @@ private function writeConfigEnabled(string $path, bool $value, array &$config):
$config['enabled'] = $value;
}

private function getPublicDirectoryName(ContainerBuilder $container): string
private function getPublicDirectory(ContainerBuilder $container): string
{
$defaultPublicDir = 'public';
$projectDir = $container->getParameter('kernel.project_dir');
$defaultPublicDir = $projectDir.'/public';

$composerFilePath = $container->getParameter('kernel.project_dir').'/composer.json';
$composerFilePath = $projectDir.'/composer.json';

if (!file_exists($composerFilePath)) {
return $defaultPublicDir;
Expand All @@ -3176,6 +3181,6 @@ private function getPublicDirectoryName(ContainerBuilder $container): string
$container->addResource(new FileResource($composerFilePath));
$composerConfig = json_decode(file_get_contents($composerFilePath), true);

return $composerConfig['extra']['public-dir'] ?? $defaultPublicDir;
return isset($composerConfig['extra']['public-dir']) ? $projectDir.'/'.$composerConfig['extra']['public-dir'] : $defaultPublicDir;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use Symfony\Component\AssetMapper\Command\ImportMapRemoveCommand;
use Symfony\Component\AssetMapper\Command\ImportMapRequireCommand;
use Symfony\Component\AssetMapper\Command\ImportMapUpdateCommand;
use Symfony\Component\AssetMapper\CompiledAssetMapperConfigReader;
use Symfony\Component\AssetMapper\Compiler\CssAssetUrlCompiler;
use Symfony\Component\AssetMapper\Compiler\JavaScriptImportPathCompiler;
use Symfony\Component\AssetMapper\Compiler\SourceMappingUrlsCompiler;
Expand All @@ -40,6 +41,7 @@
use Symfony\Component\AssetMapper\ImportMap\RemotePackageStorage;
use Symfony\Component\AssetMapper\ImportMap\Resolver\JsDelivrEsmResolver;
use Symfony\Component\AssetMapper\MapperAwareAssetPackage;
use Symfony\Component\AssetMapper\Path\LocalPublicAssetsFilesystem;
use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolver;

return static function (ContainerConfigurator $container) {
Expand All @@ -48,7 +50,7 @@
->args([
service('asset_mapper.repository'),
service('asset_mapper.mapped_asset_factory'),
service('asset_mapper.public_assets_path_resolver'),
service('asset_mapper.compiled_asset_mapper_config_reader'),
])
->alias(AssetMapperInterface::class, 'asset_mapper')

Expand Down Expand Up @@ -76,9 +78,17 @@

->set('asset_mapper.public_assets_path_resolver', PublicAssetsPathResolver::class)
->args([
param('kernel.project_dir'),
abstract_arg('asset public prefix'),
abstract_arg('public directory name'),
])

->set('asset_mapper.local_public_assets_filesystem', LocalPublicAssetsFilesystem::class)
->args([
abstract_arg('public directory'),
])

->set('asset_mapper.compiled_asset_mapper_config_reader', CompiledAssetMapperConfigReader::class)
->args([
abstract_arg('public assets directory'),
])

->set('asset_mapper.asset_package', MapperAwareAssetPackage::class)
Expand All @@ -100,12 +110,11 @@

->set('asset_mapper.command.compile', AssetMapperCompileCommand::class)
->args([
service('asset_mapper.public_assets_path_resolver'),
service('asset_mapper.compiled_asset_mapper_config_reader'),
service('asset_mapper'),
service('asset_mapper.importmap.generator'),
service('filesystem'),
service('asset_mapper.local_public_assets_filesystem'),
param('kernel.project_dir'),
abstract_arg('public directory name'),
param('kernel.debug'),
service('event_dispatcher')->nullOnInvalid(),
])
Expand Down Expand Up @@ -163,7 +172,7 @@
->set('asset_mapper.importmap.generator', ImportMapGenerator::class)
->args([
service('asset_mapper'),
service('asset_mapper.public_assets_path_resolver'),
service('asset_mapper.compiled_asset_mapper_config_reader'),
service('asset_mapper.importmap.config_reader'),
])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public function testAssetMapper()
$container = $this->createContainerFromFile('asset_mapper');

$definition = $container->getDefinition('asset_mapper.public_assets_path_resolver');
$this->assertSame('/assets_path/', $definition->getArgument(1));
$this->assertSame('/assets_path/', $definition->getArgument(0));

$definition = $container->getDefinition('asset_mapper.dev_server_subscriber');
$this->assertSame(['zip' => 'application/zip'], $definition->getArgument(2));
Expand Down
9 changes: 3 additions & 6 deletions src/Symfony/Component/AssetMapper/AssetMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
namespace Symfony\Component\AssetMapper;

use Symfony\Component\AssetMapper\Factory\MappedAssetFactoryInterface;
use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolverInterface;

/**
* Finds and returns assets in the pipeline.
Expand All @@ -28,7 +27,7 @@ class AssetMapper implements AssetMapperInterface
public function __construct(
private readonly AssetMapperRepository $mapperRepository,
private readonly MappedAssetFactoryInterface $mappedAssetFactory,
private readonly PublicAssetsPathResolverInterface $assetsPathResolver,
private readonly CompiledAssetMapperConfigReader $compiledConfigReader,
) {
}

Expand Down Expand Up @@ -78,12 +77,10 @@ public function getPublicPath(string $logicalPath): ?string
private function loadManifest(): array
{
if (null === $this->manifestData) {
$path = $this->assetsPathResolver->getPublicFilesystemPath().'/'.self::MANIFEST_FILE_NAME;

if (!is_file($path)) {
if (!$this->compiledConfigReader->configExists(self::MANIFEST_FILE_NAME)) {
$this->manifestData = [];
} else {
$this->manifestData = json_decode(file_get_contents($path), true);
$this->manifestData = $this->compiledConfigReader->loadConfig(self::MANIFEST_FILE_NAME);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,15 @@

use Symfony\Component\AssetMapper\AssetMapper;
use Symfony\Component\AssetMapper\AssetMapperInterface;
use Symfony\Component\AssetMapper\CompiledAssetMapperConfigReader;
use Symfony\Component\AssetMapper\Event\PreAssetsCompileEvent;
use Symfony\Component\AssetMapper\ImportMap\ImportMapGenerator;
use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolverInterface;
use Symfony\Component\AssetMapper\Path\PublicAssetsFilesystemInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

/**
Expand All @@ -36,12 +35,11 @@
final class AssetMapperCompileCommand extends Command
{
public function __construct(
private readonly PublicAssetsPathResolverInterface $publicAssetsPathResolver,
private readonly CompiledAssetMapperConfigReader $compiledConfigReader,
private readonly AssetMapperInterface $assetMapper,
private readonly ImportMapGenerator $importMapGenerator,
private readonly Filesystem $filesystem,
private readonly PublicAssetsFilesystemInterface $assetsFilesystem,
private readonly string $projectDir,
private readonly string $publicDirName,
private readonly bool $isDebug,
private readonly ?EventDispatcherInterface $eventDispatcher = null,
) {
Expand All @@ -51,7 +49,6 @@ public function __construct(
protected function configure(): void
{
$this
->addOption('clean', null, null, 'Whether to clean the public directory before compiling assets')
->setHelp(<<<'EOT'
The <info>%command.name%</info> command compiles and dumps all the assets in
the asset mapper into the final public directory (usually <comment>public/assets</comment>).
Expand All @@ -64,61 +61,36 @@ protected function configure(): void
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$publicDir = $this->projectDir.'/'.$this->publicDirName;
if (!is_dir($publicDir)) {
throw new InvalidArgumentException(sprintf('The public directory "%s" does not exist.', $publicDir));
}

$outputDir = $this->publicAssetsPathResolver->getPublicFilesystemPath();
if ($input->getOption('clean')) {
$io->comment(sprintf('Cleaning <info>%s</info>', $outputDir));
$this->filesystem->remove($outputDir);
$this->filesystem->mkdir($outputDir);
}

// set up the file paths
$files = [];
$manifestPath = $outputDir.'/'.AssetMapper::MANIFEST_FILE_NAME;
$files[] = $manifestPath;

$importMapPath = $outputDir.'/'.ImportMapGenerator::IMPORT_MAP_CACHE_FILENAME;
$files[] = $importMapPath;
$this->eventDispatcher?->dispatch(new PreAssetsCompileEvent($output));

$entrypointFilePaths = [];
// remove existing config files
$this->compiledConfigReader->removeConfig(AssetMapper::MANIFEST_FILE_NAME);
$this->compiledConfigReader->removeConfig(ImportMapGenerator::IMPORT_MAP_CACHE_FILENAME);
$entrypointFiles = [];
foreach ($this->importMapGenerator->getEntrypointNames() as $entrypointName) {
$dumpedEntrypointPath = $outputDir.'/'.sprintf(ImportMapGenerator::ENTRYPOINT_CACHE_FILENAME_PATTERN, $entrypointName);
$files[] = $dumpedEntrypointPath;
$entrypointFilePaths[$entrypointName] = $dumpedEntrypointPath;
$path = sprintf(ImportMapGenerator::ENTRYPOINT_CACHE_FILENAME_PATTERN, $entrypointName);
$this->compiledConfigReader->removeConfig($path);
$entrypointFiles[$entrypointName] = $path;
}

// remove existing files
foreach ($files as $file) {
if (is_file($file)) {
$this->filesystem->remove($file);
}
}

$this->eventDispatcher?->dispatch(new PreAssetsCompileEvent($outputDir, $output));

// dump new files
$manifest = $this->createManifestAndWriteFiles($io, $publicDir);
$this->filesystem->dumpFile($manifestPath, json_encode($manifest, \JSON_PRETTY_PRINT));
$manifest = $this->createManifestAndWriteFiles($io);
$manifestPath = $this->compiledConfigReader->saveConfig(AssetMapper::MANIFEST_FILE_NAME, $manifest);
$io->comment(sprintf('Manifest written to <info>%s</info>', $this->shortenPath($manifestPath)));

$this->filesystem->dumpFile($importMapPath, json_encode($this->importMapGenerator->getRawImportMapData(), \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_HEX_TAG));
$importMapPath = $this->compiledConfigReader->saveConfig(ImportMapGenerator::IMPORT_MAP_CACHE_FILENAME, $this->importMapGenerator->getRawImportMapData());
$io->comment(sprintf('Import map data written to <info>%s</info>.', $this->shortenPath($importMapPath)));

$entrypointNames = $this->importMapGenerator->getEntrypointNames();
foreach ($entrypointFilePaths as $entrypointName => $path) {
$this->filesystem->dumpFile($path, json_encode($this->importMapGenerator->findEagerEntrypointImports($entrypointName), \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_HEX_TAG));
foreach ($entrypointFiles as $entrypointName => $path) {
$this->compiledConfigReader->saveConfig($path, $this->importMapGenerator->findEagerEntrypointImports($entrypointName));
}
$styledEntrypointNames = array_map(fn (string $entrypointName) => sprintf('<info>%s</>', $entrypointName), $entrypointNames);
$io->comment(sprintf('Entrypoint metadata written for <comment>%d</> entrypoints (%s).', \count($entrypointNames), implode(', ', $styledEntrypointNames)));
$styledEntrypointNames = array_map(fn (string $entrypointName) => sprintf('<info>%s</>', $entrypointName), array_keys($entrypointFiles));
$io->comment(sprintf('Entrypoint metadata written for <comment>%d</> entrypoints (%s).', \count($entrypointFiles), implode(', ', $styledEntrypointNames)));

if ($this->isDebug) {
$io->warning(sprintf(
'You are compiling assets in development. Symfony will not serve any changed assets until you delete the "%s" directory.',
$this->shortenPath($outputDir)
'You are compiling assets in development. Symfony will not serve any changed assets until you delete the files in the "%s" directory.',
$this->shortenPath(\dirname($manifestPath))
));
}

Expand All @@ -130,20 +102,18 @@ private function shortenPath(string $path): string
return str_replace($this->projectDir.'/', '', $path);
}

private function createManifestAndWriteFiles(SymfonyStyle $io, string $publicDir): array
private function createManifestAndWriteFiles(SymfonyStyle $io): array
{
$allAssets = $this->assetMapper->allAssets();

$io->comment(sprintf('Compiling assets to <info>%s%s</info>', $publicDir, $this->publicAssetsPathResolver->resolvePublicPath('')));
$io->comment(sprintf('Compiling and writing asset files to <info>%s</info>', $this->shortenPath($this->assetsFilesystem->getDestinationPath())));
$manifest = [];
foreach ($allAssets as $asset) {
// $asset->getPublicPath() will start with a "/"
$targetPath = $publicDir.$asset->publicPath;
if (null !== $asset->content) {
// The original content has been modified by the AssetMapperCompiler
$this->filesystem->dumpFile($targetPath, $asset->content);
$this->assetsFilesystem->write($asset->publicPath, $asset->content);
} else {
$this->filesystem->copy($asset->sourcePath, $targetPath, true);
$this->assetsFilesystem->copy($asset->sourcePath, $asset->publicPath);
}

$manifest[$asset->logicalPath] = $asset->publicPath;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\AssetMapper;

use Symfony\Component\Filesystem\Path;

/**
* Reads and writes compiled configuration files for asset mapper.
*/
class CompiledAssetMapperConfigReader
{
public function __construct(private readonly string $directory)
{
}

public function configExists(string $filename): bool
{
return is_file(Path::join($this->directory, $filename));
}

public function loadConfig(string $filename): array
{
return json_decode(file_get_contents(Path::join($this->directory, $filename)), true, 512, \JSON_THROW_ON_ERROR);
}

public function saveConfig(string $filename, array $data): string
{
$path = Path::join($this->directory, $filename);
@mkdir(\dirname($path), 0777, true);
file_put_contents($path, json_encode($data, \JSON_PRETTY_PRINT | \JSON_THROW_ON_ERROR));

return $path;
}

public function removeConfig(string $filename): void
{
$path = Path::join($this->directory, $filename);

if (is_file($path)) {
unlink($path);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,13 @@
*/
class PreAssetsCompileEvent extends Event
{
private string $outputDir;
private OutputInterface $output;

public function __construct(string $outputDir, OutputInterface $output)
public function __construct(OutputInterface $output)
{
$this->outputDir = $outputDir;
$this->output = $output;
}

public function getOutputDir(): string
{
return $this->outputDir;
}

public function getOutput(): OutputInterface
{
return $this->output;
Expand Down

0 comments on commit d261eeb

Please sign in to comment.