Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for "optional" re-exports #30

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

nicolo-ribaudo
Copy link
Member

@nicolo-ribaudo nicolo-ribaudo commented Mar 22, 2024

Note

See #31 for an alternative. Differences between the two PRs are marked with 🔎

export optional { foo, bar } from "./there.js"

This PR adds a new keyword for exports, 🔎optional, to mark export ... from statements as three-shakeable. It has the following properties:

  • optional re-exports are only loaded if they are imported
  • optional exports are always evaluated later than the module that imported them (modulo cycles)
    • this guarantees that a module's behavior does not depend on any of its optional re-exports running. This is important because which optional re-exports run and which do not doesn't depend by the module itself, but by its consumers
  • export * from cannot be optional
    • this is because optional re-exports must explicitly define which bindings they are exporting, so that the re-exported file doesn't need to be loaded to know it
  • 🔎 the optional modifier is indipendent from modifier that affect how the imported module is represented, so all the following variations are valid:
    • export optional * as x from "x" - only load x if the consumer is importing it. Eagerly link/execute it and build the namespace object.
    • export defer * as x from "x" - always load x and its dependencies, eagerly execute it's asynchronous subgraphs, and only execute the rest on property access on the namespace object
    • export optional defer * as x from "x" - only load x if the consumer is importing it. If it gets loaded, eagerly execute it's asynchronous subgraphs, and only execute the rest on property access on the namespace object.
    • export optional source x from "x" - only load x if the consumer is importing it, and do so as defined in https://github.com/tc39/proposal-source-phase-imports

An optional re-export is considered to be used if the consumer of the module is:

  • importing with import * as (🔎 or import defer * as)
  • importing with import { ... } and listing the optional binding
  • importing with import(...) (🔎 or import.defer(...))

A follow-up proposal might introduce some syntax to mark which bindings will be used in namespace imports, such as import { foo, bar } as partialNamespace from "x" and import("x", { bindings: ["foo", "bar"] }).

A bundler/transpiler can re-write optional re-exports to these exact semantics by moving them to the consumer module:

OriginalRewritten
// main.js
import { val, bar } from "./dep.js";
import "other";

// dep.js
export let val = 2;
export { foo } from "a";
export optional { bar } from "b";
export optional { baz } from "c";
// main.js
import { val } from "./dep.js";
import { bar } from "b";
import "other";

// dep.js
export let val = 2;
export { foo } from "a";
// main.js
import * as ns from "./dep.js";

// dep.js
export let val = 2;
export { foo } from "a";
export optional { bar } from "b";
export optional { baz } from "c";
// main.js
import * as _n1 from "./dep.js";
import * as _n2 from "b";
import * as _n2 from "c";
    
const ns = Object.freeze({
  __proto__: null,
  get val() { return _n1.val; },
  get foo() { return _n1.foo; },
  get bar() { return _n2.bar; },
  get baz() { return _n3.baz; },
})

// dep.js
export let val = 2;
export { foo } from "a";

🔎

// main.js
import defer * as ns from "./dep.js";

// dep.js
export let val = 2;
export { foo } from "a";
export optional { bar } from "b";
export optional { baz } from "c";
// main.js
import defer * as _n1 from "./dep.js";
import defer * as _n2 from "b";
import defer * as _n2 from "c";

const _run = () => { _n1.val, _n2.val, _n3.val; }
const ns = Object.freeze({
  __proto__: null,
  get val() { _run(); return _n1.val; },
  get foo() { _run(); return _n1.foo; },
  get bar() { _run(); return _n2.bar; },
  get baz() { _run(); return _n3.baz; },
})

// dep.js
export let val = 2;
export { foo } from "a";

Rendered preview: https://nicolo-ribaudo.github.io/proposal-defer-import-eval/branch/optional-exports.html

This was referenced Mar 22, 2024
nicolo-ribaudo added a commit to nicolo-ribaudo/proposal-defer-import-eval that referenced this pull request Mar 22, 2024
tc39#30 and tc39#31, that implement more general "optional/deferred
re-exports" with tree-shaking capabilities, give two different
meaning to `export defer * as x from "x"`:
- in tc39#30, `export defer * as x from "x"` unconditionally loads  `"x"`,
  and defers it's execution until when the namespace is used
- in tc39#31, it only loads `x` if some module is actually importing `{ x }`
  from this one, and then defers its execution

Due to this difference, for now it's better to remove `export defer *`
until its semantics are settlet, together with the other `export defer`/
`export optional` cases. I will include a revert for this commit in
those two PRs.
nicolo-ribaudo added a commit to nicolo-ribaudo/proposal-defer-import-eval that referenced this pull request Mar 22, 2024
\tc39#30 and tc39#31, that implement more general "optional/deferred
re-exports" with tree-shaking capabilities, give two different
meaning to `export defer * as x from "x"`:
- in tc39#30, `export defer * as x from "x"` unconditionally loads  `"x"`,
  and defers it's execution until when the namespace is used
- in tc39#31, it only loads `x` if some module is actually importing `{ x }`
  from this one, and then defers its execution

Due to this difference, for now it's better to remove `export defer *`
until its semantics are settlet, together with the other `export defer`/
`export optional` cases. I will include a revert for this commit in
those two PRs.
@guybedford
Copy link
Collaborator

I really like this framing of a new keyword for optional, I think point (4) in #31 already speaks to why this makes more sense semantically and in how it might compose with the other phases.

The idea of a partial deferred namespace is very interesting and I remember Yulia wanting to see something like this in the past. Would be interesting to explore that design space further as well!

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.

2 participants