Skip to content
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
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,14 @@ try {

## Documentation

The complete documentation is available at [altophp.com/importmap](https://altophp.com/importmap).
- [Installation](docs/installation.md): install the package and verify its requirements.
- [Getting started](docs/getting-started.md): build and render a first import map.
- [Imports](docs/imports.md): define and resolve exact, prefix, and scoped imports.
- [Packages](docs/packages.md): load and combine maps contributed by packages.
- [Output](docs/output.md): generate safe import-map and module-preload tags.
- [Caching](docs/caching.md): cache generated output without hiding map changes.
- [Errors](docs/errors.md): recover from invalid entries, files, and resolutions.
- [Complete documentation](docs/index.md): review the package scope and every guide.

## Development

Expand Down
34 changes: 34 additions & 0 deletions docs/caching.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Caching

Alto Importmap does not include a cache layer. Cache the rendered output in the
application when rebuilding the same complete map is expensive. Derive the key
after every package map and application override has been merged.

```php
<?php

require __DIR__.'/vendor/autoload.php';

use Alto\ImportMap\ImportMap;
use Alto\ImportMap\Renderer\HtmlRenderer;

$map = new ImportMap(imports: ['app' => '/assets/app.js']);
$state = json_encode($map, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
$key = 'importmap.'.hash('sha256', $state);

$cache = [];
$html = $cache[$key] ??= (new HtmlRenderer())->render($map);

echo $key.PHP_EOL;
echo $html;
```

The key changes when the serialized imports, scopes, integrity metadata, or
their order changes. If maps come from files, package manifests, deployment
metadata, or environment-specific URLs, include their resulting map state in
the key rather than relying only on a fixed cache name.

`ImportMap` is mutable. Calling `add()`, `addEntry()`, or `merge()` after output
has been cached does not invalidate application storage. Finish the map first,
then derive the key and render it. Cache import-map HTML and other page output
under separate keys when their preload settings differ.
23 changes: 23 additions & 0 deletions docs/errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Errors

Package exceptions implement `ExceptionInterface`. Catch a precise exception
when the application can recover, or catch the interface at a request, job, or
command boundary.

| Failure | Cause | Recovery |
| --- | --- | --- |
| `InvalidEntryException` | Empty specifier, or a prefix specifier whose non-null address does not end in `/` | Supply a non-empty key and keep both sides of prefix mappings slash-terminated |
| `SpecifierNotFoundException` | No scoped, top-level, or URL-like match exists | Check spelling and add the intended mapping |
| `ResolutionFailedException` | An exact mapping deliberately contains `null` | Remove or replace the block only when the application should permit that dependency |
| `JsonException` | `fromJson()` receives invalid JSON, or output contains bytes JSON cannot encode | Correct the input and preserve valid UTF-8 data |
| `RuntimeException` | `fromFile()` cannot find or read its path | Check the application-authorized path and permissions |

`SpecifierNotFoundException` extends `ResolutionFailedException`, so catching
the parent handles both missing and blocked imports. When scoped resolution is
unexpected, verify the referencing URL: the longest matching scope takes
priority, then resolution falls back through shorter scopes and top-level
imports.

The package does not verify that a resolved address exists or that a browser
can fetch it. For browser failures, inspect the rendered map, document base URL,
network response, and content security policy in the consuming application.
3 changes: 2 additions & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ The renderer produces an import-map script followed by preload links for top-lev

`ImportMap` is mutable: `add()`, `addEntry()`, and `merge()` update the current map and return it for chaining.

Next, define more complete [maps](maps.md), configure [rendering](rendering.md), or resolve specifiers in PHP with the [resolver](resolution.md).
Next, define and resolve [imports](imports.md), combine [package maps](packages.md),
or configure browser [output](output.md).
105 changes: 105 additions & 0 deletions docs/imports.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Imports

An `ImportMap` contains three public collections:

- `imports`: top-level `specifier => address` mappings;
- `scopes`: scope prefixes containing their own mappings;
- `integrity`: resolved addresses mapped to integrity metadata.

Addresses may be strings or `null`. A null exact mapping deliberately blocks
resolution.

## Add entries

```php
<?php

require __DIR__.'/vendor/autoload.php';

use Alto\ImportMap\ImportMap;
use Alto\ImportMap\MapEntry;

$map = new ImportMap();
$map
->add('app', '/assets/app.js', integrity: 'sha384-example')
->add('vendor/', '/assets/vendor/')
->add('legacy', null)
->add('ui', '/assets/admin-ui.js', scope: '/admin/');

$map->addEntry(
new MapEntry('charts', '/assets/charts.js'),
scope: '/reports/',
);

echo json_encode($map, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR), "\n";
```

The output is:

```text
{
"imports": {
"app": "\/assets\/app.js",
"vendor\/": "\/assets\/vendor\/",
"legacy": null
},
"scopes": {
"\/admin\/": {
"ui": "\/assets\/admin-ui.js"
},
"\/reports\/": {
"charts": "\/assets\/charts.js"
}
},
"integrity": {
"\/assets\/app.js": "sha384-example"
}
}
```

`add()` is a shortcut around `addEntry()`. Both methods mutate the current map
and return it for chaining.

An empty specifier throws `InvalidEntryException`. When a specifier ends with
`/`, its non-null address must also end with `/`; this keeps prefix resolution
well-defined. Integrity is stored by address, so it follows that exact resolved
address across top-level and scoped entries.

## Resolve imports

`NativeResolver` resolves an import in PHP and returns its address with nullable
integrity metadata.

```php
<?php

require __DIR__.'/vendor/autoload.php';

use Alto\ImportMap\Resolver\NativeResolver;

$resolver = new NativeResolver();

$global = $resolver->resolve('app', $map);
$dependency = $resolver->resolve('vendor/router.js', $map);
$scoped = $resolver->resolve('ui', $map, '/admin/dashboard.js');

echo $global['url'].PHP_EOL;
echo $global['integrity'].PHP_EOL;
echo $dependency['url'].PHP_EOL;
echo $scoped['url'].PHP_EOL;
```

The output is:

```text
/assets/app.js
sha384-example
/assets/vendor/router.js
/assets/admin-ui.js
```

Resolution checks matching scopes from longest to shortest, then top-level
imports, then passes through absolute URLs and absolute or relative paths. An
exact match wins over a prefix, and the longest matching prefix wins. The PHP
resolver does not fetch a module, resolve a relative address against a browser
document, or prove that an address exists.
28 changes: 19 additions & 9 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
# Alto Importmap

Alto Importmap builds, loads, combines, renders, and resolves JavaScript import maps in PHP. It supports top-level imports, scopes, integrity metadata, null mappings, and module preloads.
Alto Importmap builds, combines, renders, and resolves JavaScript import maps
in PHP. It supports exact and prefix imports, scopes, integrity metadata,
blocked entries, and module preloads.

## Introduction
```php
use Alto\ImportMap\ImportMap;

- [Installation](installation.md) installs the package and lists its requirements.
- [Getting started](getting-started.md) builds and renders a first map.
$map = new ImportMap();
$map->add('app', '/assets/app.js');
```

## Import maps
## Documentation

- [Maps](maps.md) covers entries, scopes, integrity metadata, JSON, and merging.
- [Rendering](rendering.md) generates safe import-map and module-preload tags.
- [Resolution](resolution.md) resolves exact, prefix, scoped, and URL-like specifiers.
- [Installation](installation.md): install the package and verify its requirements.
- [Getting started](getting-started.md): build and render a first import map.
- [Imports](imports.md): define and resolve exact, prefix, and scoped imports.
- [Packages](packages.md): load and combine maps contributed by packages.
- [Output](output.md): generate safe import-map and module-preload tags.
- [Caching](caching.md): cache generated output without hiding map changes.
- [Errors](errors.md): recover from invalid entries, files, and resolutions.

The package represents import maps and returns HTML or resolution data. It does not download JavaScript modules or write configuration files.
The package represents import maps and returns HTML or resolution data. It does
not download JavaScript modules, verify remote content, or write configuration
files.
92 changes: 0 additions & 92 deletions docs/maps.md

This file was deleted.

43 changes: 43 additions & 0 deletions docs/output.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Output

`HtmlRenderer::render(ImportMap $map, bool $preload = true): string` produces an
import-map script and, by default, module-preload links.

```php
<?php

require __DIR__.'/vendor/autoload.php';

use Alto\ImportMap\ImportMap;
use Alto\ImportMap\Renderer\HtmlRenderer;

$map = new ImportMap();
$map
->add('app', '/assets/app.js', integrity: 'sha384-example')
->add('blocked', null)
->add('admin', '/assets/admin.js', scope: '/admin/');

$html = (new HtmlRenderer())->render($map, preload: true);
echo $html;
```

The output is:

```html
<script type="importmap">{"imports":{"app":"/assets/app.js","blocked":null},"scopes":{"/admin/":{"admin":"/assets/admin.js"}},"integrity":{"/assets/app.js":"sha384-example"}}</script>
<link rel="modulepreload" href="/assets/app.js">
```

The JSON includes imports, scopes, null mappings, and integrity metadata. The
renderer uses the `JSON_HEX_*` flags so map content cannot close the script
element, and escapes preload addresses for the `href` attribute.

Preload links are generated only for non-null top-level imports. Scoped entries
are not preloaded, and integrity metadata is not copied to an HTML `integrity`
attribute. Pass `preload: false` when the application uses another preload
strategy.

Insert the generated map before scripts that import its specifiers. A mapping
does not publish or download its target, and the package does not validate an
integrity hash against remote content. Custom renderers implement
`RendererInterface::render(ImportMap $map, bool $preload = true): string`.
Loading
Loading