Skip to content

Testing plan for deferred re-exports (export defer) #5010

@caiolima

Description

@caiolima

Proposal: https://github.com/tc39/proposal-deferred-reexports
Spec text: https://tc39.es/proposal-deferred-reexports/
Feature flag: deferred-reexports
Stage: 2
Champions: @nicolo-ribaudo, @caiolima

Syntax

Valid syntax:

  • export defer { x } from "mod" -- single named deferred re-export
  • export defer { x, y } from "mod" -- multiple named deferred re-exports
  • export defer { x as y } from "mod" -- renamed deferred re-export
  • export defer * as ns from "mod" -- deferred namespace re-export
  • export defer { x } from "mod" with { type: "json" } -- with import attributes

Early errors (SyntaxError):

  • export defer * from "mod" -- bare star without as is a SyntaxError
  • export defer { x } -- missing from clause (must be a re-export)
  • export defer function f() {} -- cannot defer local declarations
  • export defer default x -- cannot defer default export
  • export defer const x = 1 -- cannot defer variable declarations
  • export defer used outside of a module (script context)

Load and Evaluation

  • export defer { x } from "./dep.js" where no consumer imports x -- dep.js is never loaded and evaluated
  • export defer { x } from "./dep.js" where a consumer imports x -- dep.js is loaded and evaluated
  • Chained export defer: A has export defer { x } from "./B.js", B has export defer { x } from "./C.js" -- C is only evaluated when x is actually imported/accessed by the consumer of A
  • export defer { x } from "./dep.js" where a consumer re-exports x without defer export {x} from ... and there IS a consumer of x on entrypoint. It should trigger load and evaluation of dep.js
  • export defer { x } from "./dep.js" where a consumer re-exports x without defer export {x} from ... and there IS NO consumer of x on entrypoint. It should trigger load and evaluation of dep.js

Namespace Object

Triggering evaluation via import * as ns:

  • import * as ns from "./reexport.js" where reexport has export defer { x } from "./dep.js". Ir eargerly loads dep.js, and accessing ns.x triggers evaluation of dep.js and returns the correct value
  • Accessing a deferred export a second time does not re-trigger evaluation (module is cached). It returns same value
  • Transitive: import * as ns from "./reexport.js" where reexport has export defer { x } from "./middle.js" and middle has export defer { x } from "./deep.js". Accessing ns.x triggers evaluation through the chain
  • Acessing a deferred export that is not ready for sync execution throws aTypeError
  • import * as ns from "./reexport.js", where reexport has export defer { x } from "./dep.js``, and dep has syntax error, it eagerly throws SyntaxError, sincedep.js` will be loaded
  • Transitive: import * as ns from "./reexport.js", where reexport has transitive export defer chain and source has syntax error, it eagerly throws SyntaxError, since source.js will be loaded

Property enumeration:

  • Reflect.ownKeys(ns) includes names from export defer declarations if it comes from import * as ns ...
  • Reflect.ownKeys(ns) includes names from export defer declarations if it comes from import defer * as ns ...
  • Reflect.ownKeys(ns) includes names from export defer declarations if it comes from import("mod")
  • Reflect.ownKeys(ns) includes names from export defer declarations if it comes from import.defer("mod")

Interaction with import defer * as ns:

  • import defer * as ns from "./reexport.js" where reexport uses export defer { x } from "./dep.js" -- accessing ns.x triggers evaluation of both the reexport and the deferred dependency, returns the correct value

  • import defer * as ns from "./reexport.js" -- accessing a non-deferred binding on ns triggers evaluation of the reexport but NOT evaluation of deferred dependencies

  • import defer * as ns from "./reexport.js" where reexport has export defer -- Reflect.ownKeys(ns) triggers reexport evaluation but does NOT trigger evaluation of deferred modules

  • Load test variants that we have on import * as ns ...

Dynamic import

  • import("./reexport.js") where reexport uses export defer doesn't trigger evaluation of deferred exports (it's similar to import * as ns from "reexport.js"). The evaluation happens once the deferred re-export is acessed.
  • import("./reexport.js") where reexport uses export defer {x} from "dep.js", and dep has SyntaxError, it will result in SyntaxError, since dep.js will be loaded
  • import.defer("./reexport.js") where reexport uses export defer doesn't trigger evaluation of deferred exports (it's similar to import defer * as ns from "reexport.js"). The evaluation happens once the deferred re-export is acessed, and both the deferred module and the chain from deferred re-exports gets evaluated
  • import.defer("./reexport.js") where reexport uses export defer {x} from "dep.js", and dep has SyntaxError, it will result in SyntaxError, since dep.js will be loaded
  • export defer * as ns from "./dep.js" -- deferred namespace re-export works correctly

Evaluation Order

Basic ordering (non-deferred before deferred, deferred at end):

  • Non-deferred exports evaluate before the re-exporting module's own code; deferred exports evaluate after
  • Given a barre file export { b } from "./b.js"; export defer { a } from "./a.js"; and a entrypoint that imports a and b, the order should be: b.js -> barrel -> a.js -> entrypoint
  • Deferred exports evaluate in declaration order
  • export defer { x } from "./async-dep.js" where async-dep.js uses top-level await
  • export defer { x } from "./dep.js" combined with export { y } from "./dep.js" for the same module but different bindings
  • A cycle from a descendant of export defer that reaches back to an ancestor of export defer
  • A cycle from a descendant of export defer that reaches back to the module using export defer
  • Cyclic deferred namespace object, whe A reexports B as namespace, and B reexports A as namespace

Error Propagation

Syntax/resolution errors in deferred source modules:

  • If source has SyntaxError, but its reexport is never used, the program executes properly, because it will never be loaded
  • If source has SyntaxError, and its reexported is used, the program throws SyntaxError
  • If source has SyntaxError, and its reexport is imported via a namespace object, the program throws SyntaxError because source will be loaded

Runtime errors on source of deferred re-export:

  • If it throws during evaluation, and it's never imported, then the program never throws
  • If it throws during evaluation, and it's used, the evaluation will throw
  • If it throws during evaluation, and it is imported as namespace, it will throw on access. Subsequent access will throw the same error
  • If there are 2 differrent re-exports for x, and they are imported as 2 different namespace objects import * ns1 from "reexport1"; import * ns2 from "reexport2"; and there's an exception thrown on evaluation, both access ns1.x and ns2.x should throw the same exception.

Circular references:

  • export defer forming a cycle with its consumer throws a SyntaxError
  • Cyclic re-exports throws a SyntaxError if it's imported
  • Cyclic re-exports never throws a SyntaxError if it IS NOT imported, since it's never loaded.

Interaction with Other Features

With top-level await (async modules):

  • A deferred module with an async dependency -- the async dependencies are gathered during loading and are eagerly evaluated if it's imported by entrypoint
  • A deferred module with an async dependency -- the async dependencies are ignored during loading if it IS NOT imported by entrypoint

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions