Skip to content

DEP0022 -- Adding import maps to Django - #101

Open
matthiask wants to merge 39 commits into
django:mainfrom
matthiask:rejuvenating-form-media
Open

matthiask wants to merge 39 commits into
django:mainfrom
matthiask:rejuvenating-form-media

Conversation

@matthiask

@matthiask matthiask commented Feb 14, 2025 •

Copy link
Copy Markdown

* origin/main:
  Removed SC voting language from the document.
  Updated language for annual release cadence due to DEP 20. (django#120)
  Marked DEP 20's status as Accepted. (django#116)
  Use X in Deprecation Policy section of DEP 20 for clarity. (django#115)
  DEP 20 Adopt Annual Release Cycle (django#109)
  DEP 0019 - Technical governance for the Django project (django#107)
  Finalized DEP 0018 Dictionary-based MAILERS (ticket-35514)
  DEP 0018 Dictionary-based EMAIL_PROVIDERS (ticket-35514) (django#105)
  Marked DEP 0014 Background Workers as final
  Extended project template (django#98)
  Add django command (django#100)
  Moved DEP 0011 to superseded (django#102)
@matthiask matthiask changed the title Rejuvenating form media Adding import maps to Django Sep 24, 2026
@matthiask
matthiask marked this pull request as ready for review September 24, 2026 10:35
@matthiask matthiask changed the title Adding import maps to Django DEP0022 -- Adding import maps to Django Sep 24, 2026
Comment on lines +33 to +39
Browser support for multiple import maps is spotty at the time of writing.
Chromium and Safari support multiple import maps since early 2025, but Firefox
still only uses the first import map and ignores all others (see `MDN
<https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/importmap>`__).
This means that when we want to use import maps, we have to ensure that there's
only a single import map in the output or wait for years until we can use this
feature.

@rik rik Sep 30, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mozilla has implemented this feature and is discussing enabling it. Given the many related websites listed as broken, I'm pretty sure this will be enabled soon (I'd say in the next 6 months). There's also a proposal to include it in Interop 2027.

So I think the design constraint of only emitting one import map can be relaxed.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, that's very interesting! I didn't know that we're already at that stage of browser support.

Until a feature is baseline available we still have to wait for ~30 months. That's quite a long time so if we get this supported within a few months or something like that it is still worth doing.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

30 months is two and a half years, which means June 2029 for Baseline Widely Available. Meaning this constraint will only be useful in two versions of Django at best (Django 6.2 and Django 2028). Many more future versions of Django will need to adapt to this API constraint (or have a depreciation plan) even though there won't be any practical use case anymore.

I'm mentioning all this because of the extra contributor work that could be avoided if it's considered an ok tradeoff.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm a bit more conservative in the way I'm interpreting baseline availability. To me it means I don't have to think about it too much when the support is somewhat important. It doesn't mean I get to use it if the availability is central to the project because I definitely am serving environments where browsers and devices are lagging behind.

Not having browser support for multiple import maps isn't a visual glitch or something comparable, it's a total showstopper.

TLDR: some people would have to wait significantly longer than 30 months.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a paragraph to the Rationale section with my reasoning for supporting this in Django (a6d9a32). The import map support doesn't have to be removed later, it just becomes less essential.

Thanks for the bugzilla links!

@matthiask

Copy link
Copy Markdown
Author

@jacobtylerwalls Can I ask you to apply the Django on the Med label on this PR as well? It has been open for much longer but most of the work was done last week. Thanks in advance :)

@codingjoe codingjoe left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matthiask can I suggest adding type hints to the new APIs you are proposing? Even if they don't make it into production code, they help with the reading. For example, Importmap(integrity) expects a dict, whereas Script(integrit) would expect a string.

It will also help to focus the docstrings on their external behavior, rather than on their arguments.

@matthiask

Copy link
Copy Markdown
Author

@matthiask can I suggest adding type hints to the new APIs you are proposing? Even if they don't make it into production code, they help with the reading. For example, Importmap(integrity) expects a dict, whereas Script(integrit) would expect a string.

It will also help to focus the docstrings on their external behavior, rather than on their arguments.

Yes, sure! I just did that now.

@codingjoe codingjoe left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't do a full review yet but left some notes and tried implementing it in my package.

I used:

import typing


@typing.runtime_checkable
class ImportMapLike(typing.Protocol):
    """
    Structural type of an import map.
    """
    imports: Mapping[str, str]
    scopes: Mapping[str, Mapping[str, str]]
    integrity: Mapping[str, str]


# my concrete backport
def __or__(self, other):
   if isinstnace(other, ImportMapLike):
      # merging mappsings

My main concern is ImportMap.render(). Should it return a script-tag, or the import map JSON?

Importantly, the importmap-tag is subject to CSP. In my project is don't use neither nonce, nor inline-script for their legitimate security limitations. (Thanks for pointing those out @adamchainz).

Instead I use rendered JSON to add an explicit sha-512 digest to my script-src policy. See also: https://github.com/codingjoe/relay/blob/94847a54b2b4aaaa5b4546e958551d184c018647/root/middleware.py#L11-L44

Comment thread draft/0022-import-maps.rst Outdated
Comment on lines +83 to +88
Paths in ``imports`` and ``scopes`` are resolved when rendering, like
``Script`` paths: Relative paths are passed through ``static()``, while
URLs with a scheme and paths starting with ``/``, ``./`` or ``../`` are
used as they are. Paths ending with ``/`` are used as they are too,
since static file storages cannot resolve directories. Scope prefixes
are never resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MediaAsset uses if self._path.startswith(("http://", "https://", "/")): so relative paths will be resolved with static.

@matthiask matthiask Oct 13, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I think we probably should do the same thing Django does in HashedFilesMixin. Passing through ./ and ../ came from django-js-asset and I don't think that's very useful in this context.

EDIT: Not exactly the same, since HashedFileMixin also strips STATIC_URL if files already have it and still processes them. I don't think we should copy that feature here.

Comment on lines +108 to +115
def render(self, *, attrs: dict[str, str | bool] | None = None) -> str:
"""
Render the ``<script type="importmap">`` element. The import map
with resolved paths is serialized as JSON and escaped using
``json_script()``. ``attrs`` are added like in
``MediaAsset.render()`` (raising a ``ValueError`` for conflicting
attributes), e.g. for CSP nonces.
"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Importmaps do need a nonce, but not SRI integrity. Still, an integrity method would be great to use a hashed CSP instead of a nonce.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I added a proposal for adding a csp_hash method or something like that to the ImportMap object. integrity would be a fitting name since it returns the same data structure as proposed in DEP0021, but might be misleading. I'm not sure.

Comment thread draft/0022-import-maps.rst Outdated
Comment on lines +117 to +130
def __eq__(self, other: object) -> bool:
"""
Import maps are equal when their data and attributes are equal.
"""

def __or__(self, other: ImportMap) -> ImportMap:
"""
Return a new import map combining this import map and ``other``.

``imports``, ``integrity`` and each scope in ``scopes`` are merged
like dictionaries, so entries of ``other`` replace entries of this
import map. Attributes are merged the same way. Neither import map
is modified.
"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you don't need a docstring here, those methods are fairly self-explainatory ;)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, thanks!

Comment thread draft/0022-import-maps.rst Outdated
Comment on lines +90 to +93
For paths resolved through ``static()``, ``integrity`` metadata is added
automatically if the static files storage supports it (see `Deferred
functionality`_). ``integrity`` is only needed for other modules, e.g.
modules loaded from a CDN.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would also be limited to relative paths only, which are locaded inside staticfiles.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I reworded that slightly to explain that specifying the integrity explicitly is only needed for files loaded from elsewhere.

@matthiask

Copy link
Copy Markdown
Author

I didn't do a full review yet but left some notes and tried implementing it in my package.

I used:

import typing


@typing.runtime_checkable
class ImportMapLike(typing.Protocol):
    """
    Structural type of an import map.
    """
    imports: Mapping[str, str]
    scopes: Mapping[str, Mapping[str, str]]
    integrity: Mapping[str, str]


# my concrete backport
def __or__(self, other):
   if isinstnace(other, ImportMapLike):
      # merging mappsings

I haven't used protocols often yet, but using protocols makes sense to me. I worry a bit that this introduces some additional complexity to the DEP itself (typing.Protocol isn't used anywhere in Django right now) so I am hesitant to propose it.

My main concern is ImportMap.render(). Should it return a script-tag, or the import map JSON?

Importantly, the importmap-tag is subject to CSP. In my project is don't use neither nonce, nor inline-script for their legitimate security limitations. (Thanks for pointing those out @adamchainz).

Instead I use rendered JSON to add an explicit sha-512 digest to my script-src policy. See also: https://github.com/codingjoe/relay/blob/94847a54b2b4aaaa5b4546e958551d184c018647/root/middleware.py#L11-L44

Looks great, thanks for the link!

I think it should return a full tag as the media assets do. I don't feel too strongly about that though. I added the deferred proposal to add a csp_hash method to the import map object which you could use instead of having to extract the map from the rendered HTML again. 223b1ac also mentions that this method could return the IntegrityMetadataList from DEP0021, but proposes using csp_hash and not integrity because the later might be confusing as well, since import maps don't need SRI integrity (as you write).

@LilyFirefly

Copy link
Copy Markdown
Contributor

runtime_checkable might be an attractive nuisance, unless it has improved in recent releases: https://discuss.python.org/t/is-there-a-downside-to-typing-runtime-checkable/20731

@matthiask

Copy link
Copy Markdown
Author

runtime_checkable might be an attractive nuisance, unless it has improved in recent releases: https://discuss.python.org/t/is-there-a-downside-to-typing-runtime-checkable/20731

Thanks! That's interesting. That's another reason to leave it out for now. Adding a factory function which converts an ImportMapLike to an ImportMap would be easy to add if we need it.

@codingjoe

codingjoe commented Oct 14, 2026 •

Copy link
Copy Markdown

I don't know if I linked this here before, but just in case: mdn/browser-compat-data#30774
However, this is no holdback for us. Just DEP21 and DEP22 in conjunction will not work until the Safari patch is readily available.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants