Skip to content

[AssetMapper] Adding "path" option to importmap:require #50363

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@
->args([
service('asset_mapper.importmap.manager'),
service('asset_mapper'),
param('kernel.project_dir'),
])
->tag('console.command')

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,18 @@ public function __construct(

protected function configure(): void
{
$this->addArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'The packages to remove');
$this
->addArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'The packages to remove')
->setHelp(<<<'EOT'
The <info>%command.name%</info> command removes packages from the <comment>importmap.php</comment>.
If a package was downloaded into your app, the downloaded file will also be removed.

For example:

<info>php %command.full_name% lodash</info>
EOT
)
;
}

protected function execute(InputInterface $input, OutputInterface $output): int
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,26 +35,65 @@ final class ImportMapRequireCommand extends Command
public function __construct(
private readonly ImportMapManager $importMapManager,
private readonly AssetMapperInterface $assetMapper,
private readonly string $projectDir,
) {
parent::__construct();
}

protected function configure(): void
{
$this->addArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'The packages to add');
$this->addOption('download', 'd', InputOption::VALUE_NONE, 'Download packages locally');
$this->addOption('preload', 'p', InputOption::VALUE_NONE, 'Preload packages');
$this
->addArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'The packages to add')
->addOption('download', 'd', InputOption::VALUE_NONE, 'Download packages locally')
->addOption('preload', 'p', InputOption::VALUE_NONE, 'Preload packages')
->addOption('path', null, InputOption::VALUE_REQUIRED, 'The local path where the package lives relative to the project root')
->setHelp(<<<'EOT'
The <info>%command.name%</info> command adds packages to <comment>importmap.php</comment> usually
by finding a CDN URL for the given package and version.

For example:

<info>php %command.full_name% lodash --preload</info>
<info>php %command.full_name% "lodash@^4.15"</info>

The <info>preload</info> option will set the <info>preload</info> option in the importmap,
which will tell the browser to preload the package. This should be used for all
critical packages that are needed on page load.

The <info>download</info> option will download the package locally and point the
importmap to it. Use this if you want to avoid using a CDN or if you want to
ensure that the package is available even if the CDN is down.

Sometimes, a package may require other packages and multiple new items may be added
to the import map.

You can also require multiple packages at once:

<info>php %command.full_name% "lodash@^4.15" "@hotwired/stimulus"</info>

EOT
);
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);

$packageList = $input->getArgument('packages');
if ($input->hasOption('path') && \count($packageList) > 1) {
$io->error('The "--path" option can only be used when you require a single package.');
$path = null;
if ($input->hasOption('path')) {
if (\count($packageList) > 1) {
$io->error('The "--path" option can only be used when you require a single package.');

return Command::FAILURE;
}

return Command::FAILURE;
$path = $this->projectDir.'/'.$input->getOption('path');
if (!is_file($path)) {
$io->error(sprintf('The path "%s" does not exist.', $input->getOption('path')));

return Command::FAILURE;
}
}

$packages = [];
Expand All @@ -73,6 +112,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$input->getOption('preload'),
null,
isset($parts['registry']) && $parts['registry'] ? $parts['registry'] : null,
$path,
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ public function __construct(
parent::__construct();
}

protected function configure(): void
{
$this
->setHelp(<<<'EOT'
The <info>%command.name%</info> command will update all from the 3rd part packages
in <comment>importmap.php</comment> to their latest version, including downloaded packages.

<info>php %command.full_name%</info>
EOT
);
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
Expand Down
29 changes: 27 additions & 2 deletions src/Symfony/Component/AssetMapper/ImportMap/ImportMapManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,20 @@ private function requirePackages(array $packagesToRequire, array &$importMapEntr

$installData = [];
$packageRequiresByName = [];
$addedEntries = [];
foreach ($packagesToRequire as $requireOptions) {
if (null !== $requireOptions->path) {
$newEntry = new ImportMapEntry(
$requireOptions->packageName,
$requireOptions->path,
$requireOptions->preload,
);
$importMapEntries[$requireOptions->packageName] = $newEntry;
$addedEntries[] = $newEntry;

continue;
}

$constraint = $requireOptions->packageName;
if (null !== $requireOptions->versionConstraint) {
$constraint .= '@'.$requireOptions->versionConstraint;
Expand All @@ -233,6 +246,10 @@ private function requirePackages(array $packagesToRequire, array &$importMapEntr
$packageRequiresByName[$requireOptions->packageName] = $requireOptions;
}

if (!$installData) {
return $addedEntries;
}

$json = [
'install' => $installData,
'flattenScope' => true,
Expand Down Expand Up @@ -261,7 +278,6 @@ private function requirePackages(array $packagesToRequire, array &$importMapEntr
// if we're requiring just one package, in case it has any peer deps, match the preload
$defaultPreload = 1 === \count($packagesToRequire) ? $packagesToRequire[0]->preload : false;

$addedEntries = [];
foreach ($response->toArray()['map']['imports'] as $packageName => $url) {
$requireOptions = $packageRequiresByName[$packageName] ?? null;
$importName = $requireOptions && $requireOptions->importName ? $requireOptions->importName : $packageName;
Expand Down Expand Up @@ -344,7 +360,16 @@ private function writeImportMapConfig(array $entries): void
foreach ($entries as $entry) {
$config = [];
if ($entry->path) {
$config[$entry->isDownloaded ? 'downloaded_to' : 'path'] = $entry->path;
$path = $entry->path;
// if the path is an absolute path, convert it to an asset path
if (is_file($path)) {
$asset = $this->assetMapper->getAssetFromSourcePath($path);
if (null === $asset) {
throw new \LogicException(sprintf('The "%s" importmap entry contains the path "%s" but it does not appear to be in any of your asset paths.', $entry->importName, $path));
}
$path = $asset->getLogicalPath();
}
$config[$entry->isDownloaded ? 'downloaded_to' : 'path'] = $path;
}
if ($entry->url) {
$config['url'] = $entry->url;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public function __construct(
public readonly bool $preload = false,
public readonly ?string $importName = null,
public readonly ?string $registryName = null,
public readonly ?string $path = null,
) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ protected function setUp(): void
if (!file_exists(__DIR__.'/../fixtures/importmaps_for_writing')) {
$this->filesystem->mkdir(__DIR__.'/../fixtures/importmaps_for_writing');
}
if (!file_exists(__DIR__.'/../fixtures/importmaps_for_writing/assets')) {
$this->filesystem->mkdir(__DIR__.'/../fixtures/importmaps_for_writing/assets');
}
file_put_contents(__DIR__.'/../fixtures/importmaps_for_writing/assets/some_file.js', '// some_file.js contents');
}

protected function tearDown(): void
Expand Down Expand Up @@ -261,6 +265,18 @@ public static function getRequirePackageTests(): iterable
],
'expectedDownloadedFiles' => [],
];

yield 'single_package_with_a_path' => [
'packages' => [new PackageRequireOptions('some/module', path: __DIR__.'/../fixtures/importmaps_for_writing/assets/some_file.js')],
'expectedInstallRequest' => [],
'responseMap' => [],
'expectedImportMap' => [
'some/module' => [
'path' => 'some_file.js',
],
],
'expectedDownloadedFiles' => [],
];
}

public function testRemove()
Expand Down