Skip to content

Latest commit

 

History

History
172 lines (108 loc) · 10.3 KB

spec.md

File metadata and controls

172 lines (108 loc) · 10.3 KB

Layered APIs Proto-spec

This document contains the basics of a spec for the layered APIs infrastructure discussed in the README. It is by no means finalized or formalized, but it gives implementers something to look at and possibly prototype with.

We expect that if this spec gains acceptance it would eventually be incorporated in the HTML Standard's script loading sections.

Introduction

Although layered APIs are loaded as JavaScript modules, they cannot have web-exposed source code. Their source code is an implementation detail, and depending on the browser, may not be in JavaScript.

This means we need to modify the module loading infrastructure slightly, as currently you are only able to load and evaluate module source code that you can also read with fetch(), due to the requirement for module scripts to be shared via CORS.

Layered API specifications

Layered API specifications define the following key concepts:

API identifier
A unique string by which this layered API will be imported
Exports
An ordered map of strings to JavaScript objects, where the keys must be IdentifierNames
Evaluation steps
An optional set of steps to run when the layered API is evaluated, i.e. when it is first imported

Layered API module records

We introduce a new type of Module Record, here called a Layered API Module Record.

We could define this in one of two ways:

  1. Hand-wave: reusing the existing Source Text Module Record (STMR) structure, and saying that implementations should create a STMR as if by parsing source text that would have the observable exports and evaluation steps as specified in the layered API's specification
  2. Formalize: actually create a new Module Record specification type; details below

We're not sure that formalization would actually be beneficial to implementers, but here we sketch out what such a module record would look like:

Formalized Layered API Module Record (LAMR)
[[Realm]]
Every Realm contains a full set of LAMRs
[[Environment]]
A synthetic Module Environment created and populated with bindings corresponding to the exports defined in the layered API's specification
[[Namespace]]
No special treatment needed; the ECMAScript specification machinery will lazily fill this in for us
[[HostDefined]]
Not used
GetExportedNames(exportStarSet)
Returns the list of exported names defined in the layered API's specification. (The exportStarSet can be ignored in our case, as layered APIs cannot participate in module cycles.)
ResolveExport(exportName, resolveSet)
Checks if exportName is present in the list of exported names defined in the layered API's specification; returns { [[Module]]: this LAMR, [[BindingName]]: exportName } if so, or null otherwise. (The resolveSet can be ignored since layered APIs cannot participate in module cycles.)
Instantiate()
Sets up [[Environment]] as described above
Evaluate()
Evaluates any setup code described in the layered API's specification

Pre-populating the module map

We would specify that on realm initialization, the module map is pre-populated with mappings from every std:x URL to the corresponding layered API module record. This must include std:blank, which is detailed below

A browser could implement this by lazily populating the map when the appropriate layered API is imported; this would be observably equivalent. Note that the map is only populated; moduleRecord.Evaluate() is not called until import time.

Reserved layered APIs

std:blank is a layered API that is guaranteed to be supported if layered APIs are supported by a browser. It has an API identifier of "blank", an empty list of exports, and no initialization steps. Its primary use case is feature detection.

std:none denotes a layered API that is never supported; that is, layered API specifications must not use "none" as their API identifier. It is also useful for tests and feature detection.

Modifications to module loading

We make the following changes to ensure that when loading modules, we retrieve the LAMR from the module map if the corresponding LAPI is implemented, or use the fallback URL if not.

Resolve a module specifier

Modify resolve a module specifier's first step as follows:

  1. Let parsed be the result of applying the URL parser to specifier. If parsed is not failure, then return the layered API fetching URL given parsed and script's base URL.

This impacts import statements and import() calls.

<script>'s prepare a script

In prepare a script, change step 23.6's "module" case to the following:

  1. Set url to the layered API fetching URL given url and settings object's API base URL.
  2. If url is failure, queue a task to fire an event named error at the element, and return.
  3. Fetch a module script graph given url, settings object, "script", and options.

This impacts <script type="module">'s src="" resolution.

<link rel="modulepreload">'s obtain the resource

In the "obtain the resource" algorithm for link type "modulepreload", move the current step 5 (which creates the settings object variable) up to before the current step 4. Then, insert the following steps after the current step 4:

  1. Set url to the layered API fetching URL given url and settings object's API base URL.
  2. If url is now failure, return.

This impacts <link rel="modulepreload">'s href="" resolution.

Supporting algorithm

The above modifications delegate to the following algorithm for determining the layered API fetching URL given a URL url and a URL baseURLForFallback:

  1. If url's scheme is not "std", return url.
  2. Let serialized be the result of running the URL serializer on url.
  3. Remove the leading "std:" from serialized.
  4. Let identifier be the portion of serialized before the first U+007C (|), or all of serialized if no U+007C is present.
  5. Let fallback be the portion of serialized after the first U+007C, or null if no U+007C is present.
  6. If the layered API identified by serialized is implemented by this user agent, return the result of parsing the concatenation of "std:" with identifier.
  7. If fallback is null, return failure.
  8. Return the result of parsing fallback with the base URL baseURLForFallback.

This operation maps URLs of the form std:x|y to a specific absolute URL: either std:x, or y (resolved relative to the given base).

Specification consequences

Here are some things to note about the above proto-spec:

Only impacts module loading

Because this specification only modifies module specifier resolution and module fetching, its impact is limited. Unlike blob: URLs that wrap Blob objects, std: URLs are not something that can be seen by fetch(), <img>, etc.

Indeed, it's not really accurate to treat std:x|y URLs as URLs at all. They are more just mechanisms for interfacing with the platform's module resolution system. They do not have an origin; they are not seen by service workers; they are not affected by CSP.

This is in some ways similar to blob: URLs that wrap MediaSource objects: those are handled specially in HTML's media resource fetch algorithm, and never make it to platform's general fetching infrastructure (e.g. the scheme fetch algorithm asserts that it never sees such blob: URLs).

Future work may make it possible to reuse JavaScript import specifiers in other URL-accepting contexts, e.g. people have proposed <img src="package:bootstrap/arrow.svg"> to integrate with package name maps. However, this would need separate work to integrate with std:x|y URLs, since we would need to preserve the property of these resources being opaque to hide their implementation details.

Feature detection

Feature detection of the layered API mechanism is possible using code such as the following:

<script type="module">
(async () => {
  let layeredAPIsSupported = false;
  try {
    await import("std:blank");
    layeredAPIsSupported = true;
  } catch {}

  if (!layeredAPIsSupported) {
    // Load polyfills the old-fashioned way.
  }
})();

This feature detection is asynchronous, but does not require any network round-trip.

Relative fallback URLs work

Although all of our examples tend to use absolute fallback URLs, since we anticipate fallbacks being located on CDNs or similar, relative URLs also work:

import { storage } from "std:async-local-storage|lib/polyfills/als.mjs";
<script type="module" src="std:virtual-list|/node_modules/virtual-list/element.mjs">