Skip to content

Commit c7493bb

Browse files
committed
feature symfony#61133 [AssetMapper] Add support for loading JSON using import statements (nicolas-grekas)
This PR was merged into the 7.4 branch. Discussion ---------- [AssetMapper] Add support for loading JSON using import statements | Q | A | ------------- | --- | Branch? | 7.4 | Bug fix? | no | New feature? | no | Deprecations? | no | Issues | - | License | MIT This is inspired by https://web.dev/blog/json-imports-baseline-newly-available Modern browsers [support loading JSONs](https://caniuse.com/mdn-javascript_statements_import_import_attributes_type_json) via the `import data from './foo.json' with {type: 'json'}` syntax. While this has been promoted as a new baseline, that's still not widely supported. This PR proposes to add support for a more portable alternative using `import jsonPromise from './foo.json'` instead, with some server-side assisted loader. On the client-side, one could then use the imported data by awaiting it first (the import returns a Promise): `json = await jsonPromise` Note that we already support importing css via import statements. Native support for `import './foo.css' with {type: 'css'}` exists, but that's [even less available](https://caniuse.com/mdn-javascript_statements_import_import_attributes_type_css). Commits ------- 990a44c [AssetMapper] Add support for loading JSON using import statements
2 parents da5988e + 990a44c commit c7493bb

File tree

7 files changed

+36
-22
lines changed

7 files changed

+36
-22
lines changed

src/Symfony/Component/AssetMapper/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
7.4
5+
---
6+
7+
* Add support for loading JSON using import statements
8+
49
7.3
510
---
611

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ protected function configure(): void
4343
{
4444
$this
4545
->addArgument('name', InputArgument::OPTIONAL, 'An asset name (or a path) to search for (e.g. "app")')
46-
->addOption('ext', null, InputOption::VALUE_REQUIRED, 'Filter assets by extension (e.g. "css")', null, ['js', 'css', 'png'])
46+
->addOption('ext', null, InputOption::VALUE_REQUIRED, 'Filter assets by extension (e.g. "css")', null, ['js', 'css', 'json'])
4747
->addOption('full', null, null, 'Whether to show the full paths')
4848
->addOption('vendor', null, InputOption::VALUE_NEGATABLE, 'Only show assets from vendor packages')
4949
->setHelp(<<<'EOT'

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public function getEntries(): ImportMapEntries
4949
throw new \InvalidArgumentException(\sprintf('The following keys are not valid for the importmap entry "%s": "%s". Valid keys are: "%s".', $importName, implode('", "', $invalidKeys), implode('", "', $validKeys)));
5050
}
5151

52-
$type = isset($data['type']) ? ImportMapType::tryFrom($data['type']) : ImportMapType::JS;
52+
$type = ImportMapType::tryFrom($data['type'] ?? 'js') ?? ImportMapType::JS;
5353
$isEntrypoint = $data['entrypoint'] ?? false;
5454

5555
if (isset($data['path'])) {

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

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ public function requirePackages(array $packagesToRequire, ImportMapEntries $impo
162162

163163
$newEntry = ImportMapEntry::createLocal(
164164
$requireOptions->importName,
165-
self::getImportMapTypeFromFilename($requireOptions->path),
165+
ImportMapType::tryFrom(pathinfo($path, \PATHINFO_EXTENSION)) ?? ImportMapType::JS,
166166
$path,
167167
$requireOptions->entrypoint,
168168
);
@@ -200,11 +200,6 @@ private function cleanupPackageFiles(ImportMapEntry $entry): void
200200
}
201201
}
202202

203-
private static function getImportMapTypeFromFilename(string $path): ImportMapType
204-
{
205-
return str_ends_with($path, '.css') ? ImportMapType::CSS : ImportMapType::JS;
206-
}
207-
208203
/**
209204
* Finds the MappedAsset allowing for a "logical path", relative or absolute filesystem path.
210205
*/

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

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ class ImportMapRenderer
3131
private const DEFAULT_ES_MODULE_SHIMS_POLYFILL_URL = 'https://ga.jspm.io/npm:[email protected]/dist/es-module-shims.js';
3232
private const DEFAULT_ES_MODULE_SHIMS_POLYFILL_INTEGRITY = 'sha384-ie1x72Xck445i0j4SlNJ5W5iGeL3Dpa0zD48MZopgWsjNB/lt60SuG1iduZGNnJn';
3333

34+
private const LOADER_JSON = "export default (async()=>await(await fetch('%s')).json())()";
35+
private const LOADER_CSS = "document.head.appendChild(Object.assign(document.createElement('link'),{rel:'stylesheet',href:'%s'}))";
36+
3437
public function __construct(
3538
private readonly ImportMapGenerator $importMapGenerator,
3639
private readonly ?Packages $assetPackages = null,
@@ -48,7 +51,7 @@ public function render(string|array $entryPoint, array $attributes = []): string
4851
$importMapData = $this->importMapGenerator->getImportMapData($entryPoint);
4952
$importMap = [];
5053
$modulePreloads = [];
51-
$cssLinks = [];
54+
$webLinks = [];
5255
$polyfillPath = null;
5356
foreach ($importMapData as $importName => $data) {
5457
$path = $data['path'];
@@ -70,29 +73,34 @@ public function render(string|array $entryPoint, array $attributes = []): string
7073
}
7174

7275
$preload = $data['preload'] ?? false;
73-
if ('css' !== $data['type']) {
76+
if ('json' === $data['type']) {
77+
$importMap[$importName] = 'data:application/javascript,'.str_replace('%', '%25', \sprintf(self::LOADER_JSON, addslashes($path)));
78+
if ($preload) {
79+
$webLinks[$path] = 'fetch';
80+
}
81+
} elseif ('css' !== $data['type']) {
7482
$importMap[$importName] = $path;
7583
if ($preload) {
7684
$modulePreloads[] = $path;
7785
}
7886
} elseif ($preload) {
79-
$cssLinks[] = $path;
87+
$webLinks[$path] = 'style';
8088
// importmap entry is a noop
8189
$importMap[$importName] = 'data:application/javascript,';
8290
} else {
83-
$importMap[$importName] = 'data:application/javascript,'.rawurlencode(\sprintf('document.head.appendChild(Object.assign(document.createElement("link"),{rel:"stylesheet",href:"%s"}))', addslashes($path)));
91+
$importMap[$importName] = 'data:application/javascript,'.str_replace('%', '%25', \sprintf(self::LOADER_CSS, addslashes($path)));
8492
}
8593
}
8694

8795
$output = '';
88-
foreach ($cssLinks as $url) {
89-
$url = $this->escapeAttributeValue($url);
90-
91-
$output .= "\n<link rel=\"stylesheet\" href=\"$url\">";
96+
foreach ($webLinks as $url => $as) {
97+
if ('style' === $as) {
98+
$output .= "\n<link rel=\"stylesheet\" href=\"{$this->escapeAttributeValue($url)}\">";
99+
}
92100
}
93101

94102
if (class_exists(AddLinkHeaderListener::class) && $request = $this->requestStack?->getCurrentRequest()) {
95-
$this->addWebLinkPreloads($request, $cssLinks);
103+
$this->addWebLinkPreloads($request, $webLinks);
96104
}
97105

98106
$scriptAttributes = $attributes || $this->scriptAttributes ? ' '.$this->createAttributesString($attributes) : '';
@@ -186,12 +194,17 @@ private function createAttributesString(array $attributes, string $pattern = '%s
186194
return $attributeString;
187195
}
188196

189-
private function addWebLinkPreloads(Request $request, array $cssLinks): void
197+
private function addWebLinkPreloads(Request $request, array $links): void
190198
{
191-
$cssPreloadLinks = array_map(fn ($url) => (new Link('preload', $url))->withAttribute('as', 'style'), $cssLinks);
199+
foreach ($links as $url => $as) {
200+
$links[$url] = (new Link('preload', $url))->withAttribute('as', $as);
201+
if ('fetch' === $as) {
202+
$links[$url] = $links[$url]->withAttribute('crossorigin', 'anonymous');
203+
}
204+
}
192205

193206
if (null === $linkProvider = $request->attributes->get('_links')) {
194-
$request->attributes->set('_links', new GenericLinkProvider($cssPreloadLinks));
207+
$request->attributes->set('_links', new GenericLinkProvider($links));
195208

196209
return;
197210
}
@@ -200,7 +213,7 @@ private function addWebLinkPreloads(Request $request, array $cssLinks): void
200213
return;
201214
}
202215

203-
foreach ($cssPreloadLinks as $link) {
216+
foreach ($links as $link) {
204217
$linkProvider = $linkProvider->withLink($link);
205218
}
206219

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ enum ImportMapType: string
1515
{
1616
case JS = 'js';
1717
case CSS = 'css';
18+
case JSON = 'json';
1819
}

src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapRendererTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public function testBasicRender()
9292
$this->assertStringContainsString('"app_css_preload": "data:application/javascript,', $html);
9393
$this->assertStringContainsString('<link rel="stylesheet" href="/subdirectory/assets/styles/app-preload-d1g35t.css">', $html);
9494
// non-preloaded CSS file
95-
$this->assertStringContainsString('"app_css_no_preload": "data:application/javascript,document.head.appendChild%28Object.assign%28document.createElement%28%22link%22%29%2C%7Brel%3A%22stylesheet%22%2Chref%3A%22%2Fsubdirectory%2Fassets%2Fstyles%2Fapp-nopreload-d1g35t.css%22%7D', $html);
95+
$this->assertStringContainsString('"app_css_no_preload": "data:application/javascript,document.head.appendChild(Object.assign(document.createElement(\'link\'),{rel:\'stylesheet\',href:\'/subdirectory/assets/styles/app-nopreload-d1g35t.css\'}))', $html);
9696
$this->assertStringNotContainsString('<link rel="stylesheet" href="/subdirectory/assets/styles/app-nopreload-d1g35t.css">', $html);
9797
// remote js
9898
$this->assertStringContainsString('"remote_js": "https://cdn.example.com/assets/remote-d1g35t.js"', $html);

0 commit comments

Comments
 (0)