Skip to content

Commit c11abf2

Browse files
committed
feature symfony#59464 [AssetMapper] Add --dry-run option on importmap:require command (chadyred)
This PR was merged into the 7.3 branch. Discussion ---------- [AssetMapper] Add `--dry-run` option on `importmap:require` command | Q | A | ------------- | --- | Branch? | 7.3 | Bug fix? | no | New feature? | yes | Deprecations? | no | License | MIT We are working with `@smnandre` on several ideas to **improve AssetMapper DX**, focusing here on the `importmap:require` command. Currently, there is no way to know in advance which dependent packages will be downloaded too, and the final output display lacks clarity. This PR aims to solve both issues by adding a new `--dry-run` flag and reworking the command output (with `-v` verbosity) <img width="845" alt="1" src="https://github.com/user-attachments/assets/26616802-f6f1-413f-9535-7a1a5205d277" /> <details><summary>Console output</summary> ```console php bin/console importmap:require bootstrap --dry-run -v [DRY-RUN] No changes will apply to the importmap configuration. -------------------------------------- --------- ---------------------------------------------------- Package Version Path -------------------------------------- --------- ---------------------------------------------------- bootstrap 5.3.3 assets/vendor/bootstrap/bootstrap.index.js `@popperjs`/core 2.11.8 assets/vendor/`@popperjs`/core/core.index.js bootstrap/dist/css/bootstrap.min.css 5.3.3 assets/vendor/bootstrap/dist/css/bootstrap.min.css -------------------------------------- --------- ---------------------------------------------------- [OK] 3 new items (bootstrap, `@popperjs`/core, bootstrap/dist/css/bootstrap.min.css) added to the importmap.php! [DRY-RUN] No changes applied to the importmap configuration. ``` </details> ## New `--dry-run` option This flag allows developers to simulate the installation process, without any alteration: - no modification of `importmap.php` file; - no file added or updated in the `assets` and `public` directories. The `--dry-run` flag allows developers to: - **check** commands for **mistakes** (e.g., typing `bootrap` instead of `bootstrap`, or `jquery`); - **list** dependent **packages** that will be installed (e.g., requiring `bootstrap` will also install ``@popperjs`/core`); - **test** for potential issues while **downloading**[^1] files. The console will display a line at start and end of execution to signal the dry-run mode. <img width="845" alt="2" src="https://github.com/user-attachments/assets/4df6abc0-6205-410e-9039-8c4b37a41055" /> <details><summary>Console output</summary> ```bash php bin/console importmap:require bootstrap --dry-run [DRY-RUN] No changes will apply to the importmap configuration. [OK] 3 new items (bootstrap, `@popperjs`/core, bootstrap/dist/css/bootstrap.min.css) added to the importmap.php! [DRY-RUN] No changes applied to the importmap configuration. ``` </details> ## New `-v` verbose output This PR also reworks the output for the `-v` (verbose) flag to provide more clarity when running the command with additional verbosity. <img width="845" alt="3" src="https://github.com/user-attachments/assets/46ff8d22-887c-4900-b778-36d9aaac1512" /> --- All feedback welcome! [^1]: AssetMapper recursively extracts the dependencies of JavaScript modules. Even with the `--dry-run` option, the `importmap:require` command requires a working `HttpClient` to download metadata and JavaScript files from various registries/repositories (mainly JSDelivr for now). Commits ------- 337c465 [AssetMapper] Add `--dry-run` option on `importmap:require` command
2 parents 3d4e0d6 + 337c465 commit c11abf2

File tree

8 files changed

+329
-11
lines changed

8 files changed

+329
-11
lines changed

UPGRADE-7.3.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,8 @@ VarDumper
191191

192192
* Deprecate `ResourceCaster::castCurl()`, `ResourceCaster::castGd()` and `ResourceCaster::castOpensslX509()`
193193
* Mark all casters as `@internal`
194+
195+
AssetMapper
196+
-----------
197+
198+
* `ImportMapRequireCommand` now takes `projectDir` as a required third constructor argument

src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@
232232
->args([
233233
service('asset_mapper.importmap.manager'),
234234
service('asset_mapper.importmap.version_checker'),
235+
param('kernel.project_dir'),
235236
])
236237
->tag('console.command')
237238

src/Symfony/Component/AssetMapper/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ CHANGELOG
55
---
66

77
* Add support for pre-compressing assets with Brotli, Zstandard, Zopfli, and gzip
8+
* Add option `--dry-run` to `importmap:require` command
9+
* `ImportMapRequireCommand` now takes `projectDir` as a required third constructor argument
810

911
7.2
1012
---

src/Symfony/Component/AssetMapper/Command/ImportMapRequireCommand.php

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\AssetMapper\Command;
1313

14+
use Symfony\Component\AssetMapper\ImportMap\ImportMapEntries;
1415
use Symfony\Component\AssetMapper\ImportMap\ImportMapEntry;
1516
use Symfony\Component\AssetMapper\ImportMap\ImportMapManager;
1617
use Symfony\Component\AssetMapper\ImportMap\ImportMapVersionChecker;
@@ -22,6 +23,7 @@
2223
use Symfony\Component\Console\Input\InputOption;
2324
use Symfony\Component\Console\Output\OutputInterface;
2425
use Symfony\Component\Console\Style\SymfonyStyle;
26+
use Symfony\Component\Filesystem\Path;
2527

2628
/**
2729
* @author Kévin Dunglas <[email protected]>
@@ -34,16 +36,22 @@ final class ImportMapRequireCommand extends Command
3436
public function __construct(
3537
private readonly ImportMapManager $importMapManager,
3638
private readonly ImportMapVersionChecker $importMapVersionChecker,
39+
private readonly ?string $projectDir = null,
3740
) {
41+
if (null === $projectDir) {
42+
trigger_deprecation('symfony/asset-mapper', '7.3', 'The "%s()" method will have a new `string $projectDir` argument in version 8.0, not defining it is deprecated.', __METHOD__);
43+
}
44+
3845
parent::__construct();
3946
}
4047

4148
protected function configure(): void
4249
{
4350
$this
4451
->addArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'The packages to add')
45-
->addOption('entrypoint', null, InputOption::VALUE_NONE, 'Make the package(s) an entrypoint?')
52+
->addOption('entrypoint', null, InputOption::VALUE_NONE, 'Make the packages an entrypoint?')
4653
->addOption('path', null, InputOption::VALUE_REQUIRED, 'The local path where the package lives relative to the project root')
54+
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Simulate the installation of the packages')
4755
->setHelp(<<<'EOT'
4856
The <info>%command.name%</info> command adds packages to <comment>importmap.php</comment> usually
4957
by finding a CDN URL for the given package and version.
@@ -72,6 +80,11 @@ protected function configure(): void
7280
7381
<info>php %command.full_name% "any_module_name" --path=./assets/some_file.js</info>
7482
83+
To simulate the installation, use the <info>--dry-run</info> option:
84+
85+
<info>php %command.full_name% "any_module_name" --dry-run -v</info>
86+
87+
When this option is enabled, this command does not perform any write operations to the filesystem.
7588
EOT
7689
);
7790
}
@@ -92,6 +105,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int
92105
$path = $input->getOption('path');
93106
}
94107

108+
if ($input->getOption('dry-run')) {
109+
$io->writeln(['', '<comment>[DRY-RUN]</comment> No changes will apply to the importmap configuration.', '']);
110+
}
111+
95112
$packages = [];
96113
foreach ($packageList as $packageName) {
97114
$parts = ImportMapManager::parsePackageName($packageName);
@@ -110,28 +127,45 @@ protected function execute(InputInterface $input, OutputInterface $output): int
110127
);
111128
}
112129

113-
$newPackages = $this->importMapManager->require($packages);
130+
if ($input->getOption('dry-run')) {
131+
$newPackages = $this->importMapManager->requirePackages($packages, new ImportMapEntries());
132+
} else {
133+
$newPackages = $this->importMapManager->require($packages);
134+
}
114135

115136
$this->renderVersionProblems($this->importMapVersionChecker, $output);
116137

117-
if (1 === \count($newPackages)) {
118-
$newPackage = $newPackages[0];
119-
$message = \sprintf('Package "%s" added to importmap.php', $newPackage->importName);
138+
$newPackageNames = array_map(fn (ImportMapEntry $package): string => $package->importName, $newPackages);
120139

121-
$message .= '.';
140+
if (1 === \count($newPackages)) {
141+
$messages = [\sprintf('Package "%s" added to importmap.php.', $newPackageNames[0])];
122142
} else {
123-
$names = array_map(fn (ImportMapEntry $package) => $package->importName, $newPackages);
124-
$message = \sprintf('%d new items (%s) added to the importmap.php!', \count($newPackages), implode(', ', $names));
143+
$messages = [\sprintf('%d new items (%s) added to the importmap.php!', \count($newPackages), implode(', ', $newPackageNames))];
125144
}
126145

127-
$messages = [$message];
146+
if ($io->isVerbose()) {
147+
$io->table(
148+
['Package', 'Version', 'Path'],
149+
array_map(fn (ImportMapEntry $package): array => [
150+
$package->importName,
151+
$package->version ?? '-',
152+
// BC layer for AssetMapper < 7.3
153+
// When `projectDir` is not null, we use the absolute path of the package
154+
null !== $this->projectDir ? Path::makeRelative($package->path, $this->projectDir) : $package->path,
155+
], $newPackages),
156+
);
157+
}
128158

129159
if (1 === \count($newPackages)) {
130160
$messages[] = \sprintf('Use the new package normally by importing "%s".', $newPackages[0]->importName);
131161
}
132162

133163
$io->success($messages);
134164

165+
if ($input->getOption('dry-run')) {
166+
$io->writeln(['<comment>[DRY-RUN]</comment> No changes applied to the importmap configuration.', '']);
167+
}
168+
135169
return Command::SUCCESS;
136170
}
137171
}

src/Symfony/Component/AssetMapper/ImportMap/ImportMapManager.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,13 +128,15 @@ private function updateImportMapConfig(bool $update, array $packagesToRequire, a
128128
}
129129

130130
/**
131+
* @internal
132+
*
131133
* Gets information about (and optionally downloads) the packages & updates the entries.
132134
*
133135
* Returns an array of the entries that were added.
134136
*
135137
* @param PackageRequireOptions[] $packagesToRequire
136138
*/
137-
private function requirePackages(array $packagesToRequire, ImportMapEntries $importMapEntries): array
139+
public function requirePackages(array $packagesToRequire, ImportMapEntries $importMapEntries): array
138140
{
139141
if (!$packagesToRequire) {
140142
return [];
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\AssetMapper\Tests\Command;
13+
14+
use PHPUnit\Framework\Attributes\DataProvider;
15+
use Symfony\Bundle\FrameworkBundle\Console\Application;
16+
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
17+
use Symfony\Component\AssetMapper\Command\ImportMapRequireCommand;
18+
use Symfony\Component\AssetMapper\ImportMap\ImportMapEntry;
19+
use Symfony\Component\AssetMapper\ImportMap\ImportMapManager;
20+
use Symfony\Component\AssetMapper\ImportMap\ImportMapType;
21+
use Symfony\Component\AssetMapper\ImportMap\ImportMapVersionChecker;
22+
use Symfony\Component\AssetMapper\Tests\Fixtures\ImportMapTestAppKernel;
23+
use Symfony\Component\Console\Output\OutputInterface;
24+
use Symfony\Component\Console\Tester\CommandTester;
25+
use Symfony\Component\Filesystem\Filesystem;
26+
use Symfony\Component\Finder\Finder;
27+
28+
class ImportMapRequireCommandTest extends KernelTestCase
29+
{
30+
protected static function getKernelClass(): string
31+
{
32+
return ImportMapTestAppKernel::class;
33+
}
34+
35+
/**
36+
* @dataProvider getRequirePackageTests
37+
*/
38+
public function testDryRunOptionToShowInformationBeforeApplyInstallation(int $verbosity, array $packageEntries, array $packagesToInstall, string $expected, ?string $path = null)
39+
{
40+
$importMapManager = $this->createMock(ImportMapManager::class);
41+
$importMapManager
42+
->method('requirePackages')
43+
->willReturn($packageEntries)
44+
;
45+
46+
$command = new ImportMapRequireCommand(
47+
$importMapManager,
48+
$this->createMock(ImportMapVersionChecker::class),
49+
'/path/to/project/dir',
50+
);
51+
52+
$args = [
53+
'packages' => $packagesToInstall,
54+
'--dry-run' => true,
55+
];
56+
if ($path) {
57+
$args['--path'] = $path;
58+
}
59+
60+
$commandTester = new CommandTester($command);
61+
$commandTester->execute($args, ['verbosity' => $verbosity]);
62+
63+
$commandTester->assertCommandIsSuccessful();
64+
65+
$output = $commandTester->getDisplay();
66+
$this->assertEquals($this->trimBeginEndOfEachLine($expected), $this->trimBeginEndOfEachLine($output));
67+
}
68+
69+
public static function getRequirePackageTests(): iterable
70+
{
71+
yield 'require package with dry run and normal verbosity options' => [
72+
OutputInterface::VERBOSITY_NORMAL,
73+
[self::createRemoteEntry('bootstrap', '4.2.3', 'assets/vendor/bootstrap/bootstrap.js')],
74+
['bootstrap'], <<<EOF
75+
[DRY-RUN] No changes will apply to the importmap configuration.
76+
77+
[OK] Package "bootstrap" added to importmap.php.
78+
79+
Use the new package normally by importing "bootstrap".
80+
81+
[DRY-RUN] No changes applied to the importmap configuration.
82+
EOF,
83+
];
84+
85+
yield 'remote package requested with a version with dry run and verbosity verbose options' => [
86+
OutputInterface::VERBOSITY_VERBOSE,
87+
[self::createRemoteEntry('bootstrap', '5.3.3', 'assets/vendor/bootstrap/bootstrap.js')],
88+
['bootstrap'], <<<EOF
89+
[DRY-RUN] No changes will apply to the importmap configuration.
90+
91+
----------- --------- --------------------------------------
92+
Package Version Path
93+
----------- --------- --------------------------------------
94+
bootstrap 5.3.3 assets/vendor/bootstrap/bootstrap.js
95+
----------- --------- --------------------------------------
96+
97+
[OK] Package "bootstrap" added to importmap.php.
98+
99+
Use the new package normally by importing "bootstrap".
100+
101+
[DRY-RUN] No changes applied to the importmap configuration.
102+
EOF,
103+
];
104+
105+
yield 'local package require a path, with dry run and verbosity verbose options' => [
106+
OutputInterface::VERBOSITY_VERBOSE,
107+
[ImportMapEntry::createLocal('alice.js', ImportMapType::JS, 'assets/js/alice.js', false)],
108+
['alice.js'], <<<EOF
109+
[DRY-RUN] No changes will apply to the importmap configuration.
110+
111+
---------- --------- --------------------
112+
Package Version Path
113+
---------- --------- --------------------
114+
alice.js - assets/js/alice.js
115+
---------- --------- --------------------
116+
117+
[OK] Package "alice.js" added to importmap.php.
118+
119+
Use the new package normally by importing "alice.js".
120+
121+
[DRY-RUN] No changes applied to the importmap configuration.
122+
EOF,
123+
'./assets/alice.js',
124+
];
125+
126+
yield 'multi remote packages requested with dry run and verbosity normal options' => [
127+
OutputInterface::VERBOSITY_NORMAL, [
128+
self::createRemoteEntry('bootstrap', '5.3.3', 'assets/vendor/bootstrap/bootstrap.index.js'),
129+
self::createRemoteEntry('lodash', '4.17.20', 'assets/vendor/lodash/lodash.index.js'),
130+
],
131+
['bootstrap [email protected]'], <<<EOF
132+
[DRY-RUN] No changes will apply to the importmap configuration.
133+
134+
[OK] 2 new items (bootstrap, lodash) added to the importmap.php!
135+
136+
[DRY-RUN] No changes applied to the importmap configuration.
137+
EOF,
138+
];
139+
140+
yield 'multi remote packages requested with dry run and verbosity verbose options' => [
141+
OutputInterface::VERBOSITY_VERBOSE, [
142+
self::createRemoteEntry('bootstrap', '5.3.3', 'assets/vendor/bootstrap/bootstrap.js'),
143+
self::createRemoteEntry('lodash', '4.17.20', 'assets/vendor/lodash/lodash.index.js'),
144+
],
145+
['bootstrap [email protected]'], <<<EOF
146+
[DRY-RUN] No changes will apply to the importmap configuration.
147+
148+
----------- --------- --------------------------------------
149+
Package Version Path
150+
----------- --------- --------------------------------------
151+
bootstrap 5.3.3 assets/vendor/bootstrap/bootstrap.js
152+
lodash 4.17.20 assets/vendor/lodash/lodash.index.js
153+
----------- --------- --------------------------------------
154+
155+
[OK] 2 new items (bootstrap, lodash) added to the importmap.php!
156+
157+
[DRY-RUN] No changes applied to the importmap configuration.
158+
EOF,
159+
];
160+
}
161+
162+
public function testNothingChangedOnFilesystemAfterUsingDryRunOption()
163+
{
164+
$kernel = self::bootKernel();
165+
$projectDir = $kernel->getProjectDir();
166+
167+
$fs = new Filesystem();
168+
$fs->mkdir($projectDir.'/public');
169+
170+
$fs->dumpFile($projectDir.'/public/assets/manifest.json', '{}');
171+
$fs->dumpFile($projectDir.'/public/assets/importmap.json', '{}');
172+
173+
$importMapManager = $this->createMock(ImportMapManager::class);
174+
$importMapManager
175+
->expects($this->once())
176+
->method('requirePackages')
177+
->willReturn([self::createRemoteEntry('bootstrap', '5.3.3', 'assets/vendor/bootstrap/bootstrap.index.js')]);
178+
179+
self::getContainer()->set(ImportMapManager::class, $importMapManager);
180+
181+
$application = new Application(self::$kernel);
182+
$command = $application->find('importmap:require');
183+
184+
$importMapContentBefore = $fs->readFile($projectDir.'/importmap.php');
185+
$installedVendorBefore = $fs->readFile($projectDir.'/assets/vendor/installed.php');
186+
187+
$tester = new CommandTester($command);
188+
$tester->execute(['packages' => ['bootstrap'], '--dry-run' => true]);
189+
190+
$tester->assertCommandIsSuccessful();
191+
192+
$this->assertSame($importMapContentBefore, $fs->readFile($projectDir.'/importmap.php'));
193+
$this->assertSame($installedVendorBefore, $fs->readFile($projectDir.'/assets/vendor/installed.php'));
194+
195+
$this->assertSame('{}', $fs->readFile($projectDir.'/public/assets/manifest.json'));
196+
$this->assertSame('{}', $fs->readFile($projectDir.'/public/assets/importmap.json'));
197+
198+
$finder = new Finder();
199+
$finder->in($projectDir.'/public/assets')->files()->depth(0);
200+
201+
$this->assertCount(2, $finder); // manifest.json + importmap.json
202+
203+
$fs->remove($projectDir.'/public');
204+
$fs->remove($projectDir.'/var');
205+
206+
static::$kernel->shutdown();
207+
}
208+
209+
private static function createRemoteEntry(string $importName, string $version, ?string $path = null): ImportMapEntry
210+
{
211+
return ImportMapEntry::createRemote($importName, ImportMapType::JS, path: $path, version: $version, packageModuleSpecifier: $importName, isEntrypoint: false);
212+
}
213+
214+
private function trimBeginEndOfEachLine(string $lines): string
215+
{
216+
return trim(implode("\n", array_map('trim', explode("\n", $lines))));
217+
}
218+
}

src/Symfony/Component/AssetMapper/Tests/Fixtures/AssetMapperTestAppKernel.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public function registerContainerConfiguration(LoaderInterface $loader): void
4444
'assets' => null,
4545
'asset_mapper' => [
4646
'paths' => ['dir1', 'dir2', 'non_ascii', 'assets'],
47-
'public_prefix' => 'assets'
47+
'public_prefix' => 'assets',
4848
],
4949
'test' => true,
5050
]);

0 commit comments

Comments
 (0)