Integrate `importmap`

Hi there :waving_hand:,

Little update, I wrote esimport as a base for django-esm and have my first production apps running. I am looking for early adopters to help me write an excellent documentation.

Thanks! Joe

1 Like

Hey

I have opened an issue on the new features repo for adding import map support to Django:

I have discussed the ideas in the DEP draft at the Django on the Med sprint.

Would be great to get some momentum behind this!

1 Like

Hi @matthiask! Thank you for picking up on this topic. I read the DEP and I am surprised by the choice of creating a new classe and tying it to the rhe forms and Media API. TBH, I believe it would be a mistake. First, Importmaps can be useful in projects that don’t use forms whatsoever. Secondly, I believe a declarative API resembling urls.py would be easier for IDEs to parse and provide useful autocompletion. This is the solution I proposed early on and I implemented in dj-importmap.

The two propositions are not mutually exclusive however. Nothing prevents from having a declarative API and and a programative or locally declarative one. But I believe that the backbone of the API should be an easy-to-locate and easy-to-parse solution.

Hi @christophehenry

Thanks for looking at the DEP!

I read the DEP and I am surprised by the choice of creating a new classe and tying it to the rhe forms and Media API.

This is definitely a question which also came up at the sprint. The examples aren’t clear enough then because the intention is to add basic utilities which can be used through forms (and Media) but also through other means; an import map generated e.g. by django-esm should also be allowed to be loaded into the import map delivered in the HTML.

Secondly, I believe a declarative API resembling urls.py would be easier for IDEs to parse and provide useful autocompletion. This is the solution I proposed early on and I implemented in dj-importmap.

Yep, I have definitely looked at dj-importmap. The Rationale section contains a note about using a global import map. I know that dj-importmap allows for app-scoped import maps, but that wouldn’t address some of my issues around using import maps in the Django admin (from widgets) and is also less flexible, at least if I understand the implementation correctly.

I think dj-importmap could use the ImportMap object added by this DEP. It could use the ImportMap object and the merging capabilities it already has, and the autodiscovery from importmaps.py modules could work the same way. The only additional thing is people should be aware that there could be additional import maps in a project which also have to be merged somehow.

1 Like

See the recent reframing of #22298 (Decouple Media from forms and rename it) – Django

1 Like

I think we agree on most of the proposal and that there may be a way to get the best of both worlds.

I think dj-importmap could use the ImportMap

If I understand it correctly, ImportMap is just a wrapper around a dictionnary with a few methods to easier merging importmaps. I don’t see the benefit of importmaps = ImportMap({ … }) over importmaps = { … }. Also, while your proposal enables locality that mine is missing, I believe it should be limited to specific use-cases. In projects that don’t use Node.js, it’s common to load a script from a CDN and use it throughout the project. For instance Stimulus or HTMX. Having everything in one importmaps.py makes it easier to manage CDN assets versions. While this concern could be resolved by managing them in settings.py, what I have done in the past, it appears to be less developer-friendly to me.

that wouldn’t address some of my issues around using import maps in the Django admin

Can you expand on why? If I understand DEP0022 correctly, our solutions are not very different to the exception of where we declare the importmap. Let’s take the following example from :

@admin.register(models.Question)
class QuestionModelAdmin(admin.ModelAdmin):
    class Media:
        importmap = ImportMap({"my-library": "my-app/my-library.js"})
        js = [Script("my-app/my-module.js", type="module")]

transforms to:

# importmaps.py in Django's admin app or one of the project's app
# `importmap.static` is just a lazy version of `django.templatetags.static.static`
# to avoid resolution during startup
from importmap import static

importmaps = {"my-library": static("my-app/my-library.js")}

# in admin.py
@admin.register(models.Question)
class QuestionModelAdmin(admin.ModelAdmin):
    class Media:
        js = [Script("my-app/my-module.js", type="module")]

The rest is pretty much the same.

{% csp_nonce_attr media.importmap %}

transforms to:

{% importmap nonce=csp_nonce %}

Our respective propositions don’t differ all that much. Our only point of disagreement seems to be where to preferably declare the importmaps. I sincerely belive it would be a mistake to loose importmaps.py as a preferred way to declare them.

(This reponse is already long so I will continue in another to avoid posting a big blob of text).

In a recent project, design a semi-solution for that. It basically adds MediaDefiningMixin to view-based-class. The implementation is somewhat different, though because instead of defining medias with class Media or a media property, it defined a get_media method that takes the context. It allows the dev to perform media merging, for instance in case where the context contains several media defining objects, like multiple forms. Here is an example:

class UserSettingsView(MediaDefiningMixin, UpdateView):
    def get_media(self, **context_data) -> Media:
        return (
            super().get_media(**context_data) 
           + context_data["ssh_keys_formset"].media
           + context_data["preferences_form"].media
           + context_data["oauth_apps_formset"].media
        )

Then MediaDefiningMixin automatically set media in the context.

I am unsure that it is the best solution though because it is not compatible with function-based views and hijacks context["media"]. I think, however that making media a reserved key in the context is the way forward since the context is the only object that is available in the HTML template as well as in custom template tags and filters, Form, BoundField and BoundWidget. Another solution would be to extend the rendering API to add another parameter to pass the medias.

Thanks for the thoughtful replies!

That’s not the full truth. The ImportMap I’m proposing does more than just merging imports, if DEP 0021 (SRI) is accepted it could also somewhat automatically include integrity hashes. dj-importmap currently doesn’t support integrity hashes at all if I understand the code correctly, django-js-asset (the proving ground) and the proposed DEP do support integrity hashes and scopes too.

Can you expand on why? If I understand DEP0022 correctly, our solutions are not very different to the exception of where we declare the importmap. Let’s take the following example from :slight_smile:

Yes, that’s of course correct. That said, I wish I didn’t have to define the import map in a file and define the module using it somewhere else if I use the particular import map entry in a single place only. I also think having the import map entries added automatically only when we’re actually using the module is nice. For example, in django-content-editor I have also started using import maps, but those really only have to be added to the HTML when on a particular page in the admin. What I would want for all other pages is to exclude the content_editor app’s import map from being added to the global import map. Using Media for something like this automatically handles this already, and since ES modules have to be added to Media anyway, defining the import map for those modules there as well seems like the natural place.

Our respective propositions don’t differ all that much. Our only point of disagreement seems to be where to preferably declare the importmaps. I sincerely belive it would be a mistake to loose importmaps.py as a preferred way to declare them.

I do not have anything against people doing it this way, and I think adding the automatic discovery and merging of import maps defined in importmaps.py files would be a nice feature to have for those wanting it.

Then MediaDefiningMixin automatically set media in the context.

Thanks for the code example! Something like this would probably be a good way to define a global import map and/or global media objects. It seems to be somewhat specific though, and making media a reserved word might be a larger change than what I’m trying to do.

it could also somewhat automatically include integrity hashes

Ok, that’s a fair point. I totally missed that. Then I would propose that the ImportMap class be named importmap. While it is generally a nogo to name classes with a snake case scheme, the Python standard library has a few example of classes named like this. I believe it would make an importmaps dict easier to read, semantically closer to path + urlpatterns and make it more obvious that while this is an object, this is an implementation detail and the API is designed to be declarative. I get if this is a controversial proposition though.

That said, I wish I didn’t have to define the import map in a file and define the module using it

That’s a fair point too. It would allow to declare self-sufficient components. I came across tjis use-case before. While I didn’t implement it in dj-importmap, I also wished I added a mean to declare assets (JS and CSS imports) in an HTML template too. Sometimes, frontend components are simple enough that they don't event need Python code. something like {% media js=… css=… importmap=… %} could be handy too. This however, overlaps the topic of enabling a wider use of Media so I would understand if you preferred it left out of scope.

Can I open a PR on your DEP to add my thoughts? Is it the recommended way?

Ok, that’s a fair point. I totally missed that. Then I would propose that the ImportMap class be named importmap. While it is generally a nogo to name classes with a snake case scheme, the Python standard library has a few example of classes named like this. I believe it would make an importmaps dict easier to read, semantically closer to path + urlpatterns and make it more obvious that while this is an object, this is an implementation detail and the API is designed to be declarative. I get if this is a controversial proposition though.

The StyleSheet, Script and Media classes use PascalCase so I’m not sure. The script attributes are not supposed to be mutated after the initial creation – at least I couldn’t find documentation about the attributes attribute on MediaAsset.

Also, include and path (respectively _path) aren’t classes but functions too. Are you proposing that importmap should be a function which returns some internally defined object? I’d be fine with that. For me, ImportMap seems to be closer to Script than to path, so I’m not sure about going all lowercase.

That’s a fair point too. It would allow to declare self-sufficient components. I came across tjis use-case before. While I didn’t implement it in dj-importmap, I also wished I added a mean to declare assets (JS and CSS imports) in an HTML template too. Sometimes, frontend components are simple enough that they don't event need Python code. something like {% media js=… css=… importmap=… %} could be handy too. This however, overlaps the topic of enabling a wider use of Media so I would understand if you preferred it left out of scope.

Can I open a PR on your DEP to add my thoughts? Is it the recommended way?

When you’re in the template already you could directly write script or link tags, or not? Declaring an import map this late in the process would only work in browsers supporting multiple import maps (yeah, all mayor browsers except for Firefox at the time of writing). I’d like to avoid doing the django-sekizai thing of merging the import map after rendering and injecting it at the top of the HTML after that in a second step for now. I’d certainly like to keep the potentially more controversial parts or the higher level elements of the import map support out of the DEP for now if possible, because I fear that a larger DEP might be rejected or the process delayed because of additional improvements we could implement.

Regarding a PR: Sure! I think this is a collaborative effort. You could also use the suggestion functionality directly in the GitHub pull request changes view: DEP0022 -- Adding import maps to Django by matthiask · Pull Request #101 · django/deps · GitHub . That said, I’m also happy with hashing things out here in the forum where potentially more people might participate in the discussion, at least for now.