Giter VIP home page Giter VIP logo

import-maps-extensions's Introduction

Import map extensions

Extending the import maps proposal.

Introduction

The import maps proposal is now feature-complete and working towards a stable specification and release in browsers.

In the process of attaining this stability a number of future features were deemed out of scope for the specification.

Motivation

Due to the original import maps group seeking stability, this proposal was created to enable ongoing collaboration and discussion to explore possible extensions such as those listed below, with the goal to move towards finalizing future specifications for import maps as the feature in browsers continues to evolve over time.

Proposals

Currently the following new features have been specified in this proposal:

The following additional proposals are under consideration:

Integrity

Status: Specification Pending, Implemented in SystemJS

Problem Statement

Since modules initiate requests, there is a need for the ability to specify the integrity of dependencies, and not just the top level <script type="module"> integrity which can be supported via traditional means.

For specifiers like import 'pkg' that are controlled by import maps, the problem is that the import map is fully responsible for the resolved module and hence the integrity of the resolved module as well.

Without a mechanism to specify integrity, it is not currently possible to use module dependencies with require-sri-for Content Security Policy where those module dependencies are loaded lazily so that the integrity cannot be set via the module script tag or link preload tag directly.

Proposal

An "integrity" property in the import map to allow specifying integrity for modules URLs:

{
  "integrity": {
    "/module.js": "sha384-...",
    "https://site.com/dep.js": "sha384-..."
  }
}

With the following initial semantics:

  1. The "integrity" for any module request is looked up from this import maps integrity attribute whenever there is no integrity specified.
  2. <script type="module" integrity="..."> integrity attribute on a module script will take precedence over the import map integrity.
  3. The import map integrity will only apply to modules and not other assets.

(2) Ensures that script integrity can still apply for the top-level and initial preload tags. It may be possible to define a way to resolve conflicts between these mechanisms, but an override is deemed the simplest proposal initially.

(3) avoids the need to specify the fetch option conditions that the integrity would have to apply to for other assets. It may be possible to relax this constraint in future that integrity can apply to other assets as well, but that would require more carefully defining the associated fetch conditions for which it would apply.

Alternatives

An alternative to an import map based proposal would be a more general integrity manifest applying to all types of web assets. The concern is that it is only really lazy loaded content that this integrity system is required for as in-band techniques as used currently work fine for the static use cases.

For lazy loading of other non-module assets such as stylesheets and images, in-band integrity can still apply since the dynamic injection of tags can support this fine.

By focusing only on the missing piece for modules we reduce the scope of the problem and solve the very specific issue for modules on the web which is that full integrity for deep lazy module trees is not currently possible and that this is a problem unique to module graphs.

Depcache

Status: Draft Specification, Implemented in ES Module Shims and SystemJS

Specification: https://guybedford.github.io/import-maps-extensions/#parallelizing

Implementation Status: Implemented in SystemJS

This specifies a new "depcache" field in the import map to optimize the latency waterfall of dependency discovery.

Problem Statement

Import maps form a source-of-truth for the resolution of bare module specifiers in browsers.

Dependency trees, by their nature, are designed to support arbitrary depths of dependency discovery - package A might import package B might import package C.

In addition, lazy loading of modules is a first-class feature in browsers through dynamic import() providing performance benefits in minimizing the code executed on initial page load.

The problem is that import('A') will first have to send a request over the network, before it knows that it will need to import package B, and in turn the same again for package C, incurring an unnecessary latency cost which is proportional to the dependency tree depth.

Proposal

The proposal is for modules to be able to directly declare a dependency cache upfront in the import map, as an optimization artifact created at build time (since import maps are populated by build time processes anyway):

<script type="importmap">
{
  "imports": {
    "a": "/package-a.js",
    "b": "/package-b.js",
    "c": "/package-c.js"
  },
  "depcache": {
    "/package-a.js": ["b"],
    "/package-b.js": ["c"]
  }
}
</script>

With the dependency cache populated, any time a load to import('a') is made, the browser is able to infer the deep dependency tree before the network request completes, and thus send out network requests to packages A, B and C in parallel avoiding the latency waterfall.

Alternatives

An alternative approach discussed as been a more general preload manifest that can work for all types of web assets.

The argument here is that most web assets don't typically have this level of encapsulation depth, and that this is a problem that surfaces uniquely to modules.

Isolated Scopes

Status: Experimental

Specification: Pending

Implementation Status: Pending

This proposal is about enabling resolution-level isolation properties through import maps.

Problem Statement

Import maps act as the source of truth for resolution. With a small extension to their mandate to also act as the comprehensive source of truth for what can be imported, we effectively are able to treat it as a form of resolution isolation to know without doubt that scopes cannot import from other scopes they have not been given access to.

The idea is that within a package scope, loading URLs that are child URLs of the package scope itself is permitted, but loading URLs on other origins or outside of the base-level scope would be a violation of this isolation authority, unless those mappings are explicitly provided through the scope map.

Proposal

The proposal is to provide a new "isolatedScopes": true boolean in the import map, which when enabled treats each scope as being a comprehensively isolated scope.

An isolated scope then has the following rules:

  1. Scopes cannot import URLs that are not child URLs of the scope itself, or explicit bare specifier mappings enabled within the scope.
  2. Scopes do not exhibit fallback behaviours - if there is no match for a given import, an error is thrown, rather than checking parent scopes and "imports".
  3. Isolated scopes do not permit URL mappings. This way it is easy to security audit an isolated scope since only explicit URLs and bare module specifiers need be considered to analyze the membrane boundary, rather than there also being a submapping scheme within the URL space itself. Previously discussed at WICG/import-maps#198.

The above is enough to provide simple package-level guarantees locking down importer isolation escalations with the import map.

Alternatives

The alternative is for a separate mapping system to handle the security lockdown of the resolver. This proposal exactly comes out of realising that the Node.js Policy ended up implementing mappings and scopes very similarly to import maps as part of its development and that unification might provide a path to create a security primitive from the start rather than "security as an afterthought".

Lazy Loading of Import Maps

Status: Experimental

Specification: Pending

Implementation Status: Implemented in SystemJS

This proposal extends the "waiting for import maps" phase from being a single phase at startup to being a phase that can be retriggered at any time during the execution of the page.

Problem Statement

Currently the import maps specification has an intial phase called "waiting for import maps" which corresponds to the completion of loading all import maps present on the page.

This is designed to support dynamic injection of import maps, with the phase transition out of "waiting for import maps" happening as soon as there is a import() or <script type="module"> load triggered.

As a result, the import map for the page becomes fully locked down as soon as the first module has been loaded, thereby excluding all lazy-loading in performance optimization workflows or otherwise.

Proposal

The proposal consists of two main components:

  1. Carefully defining an immutable extension mechanism for the import map.
  2. Re-triggering the "waiting for import maps" state whenever a new <script type="importmap"> is injected into the HTML page.

1. Defining Immutable Import Map Extension

It is important that the extension process does not break the idempotency requirement of the HostResolveImportedModule hook defined in the ECMA-262 specification.

There are two fields in import maps for which we need to define the immutable extension - "imports" and "scopes".

For "imports", the extension is straightforward - if a lazy-loaded map attempts to redefine an existing property of the import map, we throw a validation error.

For "scopes", we have to be a little more strict to ensure there are no possible cascades. For example, consider:

<script type="importmap">
{
  "imports": {
    "dep": "/path/to/dep.js"
  },
  "scopes": {
    "/scope/": {
      "pkg": "/path/to/pkg.js"
    }
  }
}
</script>

where later on we lazy load the new import map:

{
  "scopes": {
    "/scope/scoped-package/": {
      "dep": "/path/to/scoped-dep.js"
    }
  }
}

In the above, if we had already loaded any module from /scope/scoped-package/module.js, that contained an import 'dep' then it would have resolved differently to what is being defined in the new map, and we wouldn't necessarily know that to be the case.

To ensure cascading situations like this never break the import map immutability, we carefully define the rule for scope extension that when defining a new scope boundary, if any of the modules within that scope boundary have already been loaded in the module registry, then we throw a validation error.

As a result the above second lazy-loaded map would be a validation error if and only if /scope/scoped-package/module.js or any other module in this scope already exists in the module registry.

2. Re-triggering the "waiting for import maps" state

As soon as any new <script type="importmap"> is injected into the HTML page, we switch back into the "waiting for import maps" state exactly as defined on init.

Currently, this state causes any new top-level import operations to wait on this state before proceeding, so by re-triggering this state that same mechanism is reinvoked.

There might still be in-progress module loads in the page, which are still unresolved. These will continue to resolve with the current import map or the extended import map depending on network timing. As soon as the new import map is fully loaded it will apply to any new module resolutions immediately.

There is thus some timing dependency here, but this is mitigated by the fact that import maps are carefully defined to be immutably extensible.

The following guarantees are the primary guarantees that hold:

  1. Any <script type="module"> or dynamic import() that is called immediately after the lazy <script type="importmap"> injection, will be able to rely on the new lazily-defined maps
  2. Any existing in-progress top-level loads may or may not have these mappings available for resolutions (primarily in the case of a slow network), but they cannot rely on them.

Alternatives

An alternative approach to the "waiting problem" of lazy import map definitions would be to move the waiting period into the resolver function itself (HostResolveImportedModule).

This way, all resolve calls would be able to wait on the new import maps and we would have a full guarantee of predictability regardless of network profile.

Currently the resolver is not designed to be asynchronous in this way so this would be a larger change. In addition this would lead to an unnecessary delay for in-progress module loads since all resolutions would suddently be paused while the new import map is fetched and process. Thus, while it may seem at some theoretical level more correct, it may not be the most practical in real workflows. The primary guarantees of correctness to be relied upon is the well-defined nature of immutable map extension and when the individual top-level loads are initiated.

Acknowledgements

These extension features are entirely possible thanks to the specification and implementation work on import maps by @domenic and @hiroshige-g.

import-maps-extensions's People

Contributors

devsnek avatar domenic avatar fredkschott avatar ggoodman avatar guybedford avatar hiroshige-g avatar jkrems avatar joeldenning avatar justinfagnani avatar littledan avatar marcoscaceres avatar mylesborins avatar reod avatar rictic avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Forkers

naima-urooj

import-maps-extensions's Issues

Idempotency requirement of the HostResolveImportedModule does not seem to be preserved?

https://github.com/guybedford/import-maps-extensions#proposal-3 says

It is important that the extension process does not break the idempotency requirement of the HostResolveImportedModule hook defined in the ECMA-262 specification.

but its procedure for allowing "imports" extensions, viz.

For "imports", the extension is straightforward - if a lazy-loaded map attempts to redefine an existing property of the import map, we throw a validation error.

does not seem to respect that.

For example:

import.meta.resolve("./x"); // https://example.com/x

await loadSecondImportMap();

import.meta.resolve("./x"); // https://example.com/y

seems perfectly possible with this algorithm, if the first import map contains nothing for ./x and the second contains "./x": "./y".

(It's also possible to run into this for bare specifiers, if e.g. the first contains a mapping for "dep/" and the second contains a mapping for "dep/x". Or vice-versa. But that is probably patchable with a more complicated rule restricting such keys in the map.)

Questions about depcache

With depcache, do I understand correctly that you suggest extracting static import information to a separate file? The consequence: All static imports are known ahead of (run)time, thus, when a module is loaded, all its dependencies can be loaded simultaneously without delay.

The presented solution sacrifices space, loading a potentially large but usually moderate-size metadata file, to save on the latency induced by evaluating static imports at runtime. This evaluation could hopefully be done faster than waterfall by a functioning streaming JS interpreter but these a) do not exist yet, and b) you will never be able to beat zero-latency.

Can you imagine a situation where the depcache information would become too large to preload unconditionally?

Dynamic import maps not working if called asynchronously

I noticed weird behavior with dynamic-imports-maps extra.

let say we have 2 modules: mod1 and mod2.

import map for the first one pre-registered in index.html

<head>
...
<script type="systemjs-importmap">{"imports":{"mod1":"/mod1/main.js"}}</script>
</head>

Import map for second added dynamically:

const script = document.createElement('script');
script.setAttribute('type', 'systemjs-importmap');
script.text = JSON.stringify({"imports": {"mod2": '/mod2/main.js'}});
document.head.appendChild(script);

this works good in straight sync flow:

System.import('mod1');

const script = document.createElement('script');
script.setAttribute('type', 'systemjs-importmap');
script.text = JSON.stringify({"imports": {"mod2": '/mod2/main.js'}});
document.head.appendChild(script);

System.import('mod2');

But.

if dynamic add and import occurs asynchronously (e.g. after call or simply with setTimeout delay - import fails:

System.import('mod1');

setTimeout(() => {
  const script = document.createElement('script');
  script.setAttribute('type', 'systemjs-importmap');
  script.text = JSON.stringify({"imports": {"mod2": '/mod2/main.js'}});
  document.head.appendChild(script);

  System.import('mod2');
}, 1000);

Unhandled Promise rejection: Unable to resolve bare specifier 'mod2'

if I wrap System.import('mod2') in setTimeout - import succeed, but it looks like workaround,

Does anybody have any ideas why import behaves like that?


I use SystemJS 6.4.3

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.