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

module fragment identification #5

Closed
devsnek opened this issue Mar 7, 2021 · 10 comments · Fixed by #16
Closed

module fragment identification #5

devsnek opened this issue Mar 7, 2021 · 10 comments · Fixed by #16

Comments

@devsnek
Copy link
Member

devsnek commented Mar 7, 2021

The current string identification design is imo not a great ux, and it introduces a split between fragments and blocks. I think just using identifiers could be helpful:

module foo {}

import * from foo;
import(foo);

new Worker(module {});

export function something() {}

module bar {
  import { something } from super; // doesn't have to be "super" but it seems appropriate 
}

One thing to note is that this couldn't work, and might cause confusion:

let foo = module {};
import * from foo;

One solution would be to not have the expression form, which turns new Worker(module {}) into

module whatev {}
new Worker(whatev);

This also touches on #4 regarding how public modules (if such a thing end up existing) should be referred to from other modules.

@littledan
Copy link
Member

Would this declaration be possible to make anywhere, or only at the top level of modules?

@littledan
Copy link
Member

littledan commented Mar 7, 2021

Anyway, I really like this idea. The main thing is that I can't think of how it would extend to exported modules (maybe it would magically get the hash name?), but maybe nobody needs that. I'm OK with let foo = module {}; import * from foo; just not working; I don't think that should prevent us from having the expression form. The other issue is that it's possible that people will get queasy about some of the identifier space being static and other parts not (this was a big part of my motivation for sticking strictly with module specifiers), but the way you have it explained here, it seems well-scoped.

@devongovett
Copy link

How would you import from a module in a different file with this approach? I believe that's one of the main use cases for this proposal. I think we'd need something to be added to the module specifier to reference a sub-module and fragments make the most sense IMO. If they were declared as identifiers in the source code they could be magically mapped to fragments but this feels confusing to me. I prefer the more explicit approach of using fragment strings for this.

@devsnek
Copy link
Member Author

devsnek commented Mar 7, 2021

@devongovett yeah I'd like to avoid all the magic strings if possible... I think there are definitely solutions with the identifier form, for example import "foo".bar (I know, hideous right?). I couldn't think of something I really liked yesterday but i think we could come up with something reasonable.

@littledan
Copy link
Member

@devsnek That's a bit difficult if you need the module specifier as a string, like in a dynamic import or Worker constructor. Arguably, for those cases, you could get this as a module block object instead (if that's exported from the module), but it feels a little complicated/mismatched.

@devsnek
Copy link
Member Author

devsnek commented Mar 7, 2021

@littledan yeah I haven't though of something I really like yet. one thing which came to mind was that resource proposal, in the vein of a syntax which lets you specify the specifier and identifier and returns some opaque value which can be imported. still not great though.

@devsnek
Copy link
Member Author

devsnek commented Mar 7, 2021

I guess I'm not sold on the idea that you should be able to directly name one of these modules without going through its parent:

// a.mjs
module foo {}
export foo;

// b.mjs
import { foo } from 'a.mjs';

import * from foo;
// or
new Worker(foo);

This also explains how to import deeply nested modules (just keep adding imports) and imo is easy to understand, but it is also quite verbose.

@nayeemrmn
Copy link

nayeemrmn commented Mar 9, 2021

In the current proposal, whether a dynamic import targets an engine-held inline module or a host-resolved external one, depends on the set-inclusion of a runtime evaluated string. I can see how this is a strange structure and cost.

It makes sense to use an identifier + opaque value for inline structures, while reserving string specifiers / URLs for actual host-resolved resources.

@surma
Copy link
Member

surma commented May 13, 2021

At this point it’d make sense to consider merging this proposal with Module Blocks (or the other way around?).

One of the most-frequent questions for Module Blocks has been whether or not it tries to solve the “Bundler Problem” (putting multiple modules into one file). So far the answer has been “no”. The delta between this proposal and Module Blocks now boils down to “named” module blocks that can be used in static imports. Looking at the current README of Module Blocks, the examples would continue to work as-is, but now some could be improved to look more idiomatic (imo):

- let workerBlock = module {
+ module workerBlock {
  onmessage = function({data}) {
    let mod = await import(data);
    postMessage(mod.fn());
  }
};

let worker = new Worker(workerBlock, {type: "module"});
worker.onmessage = ({data}) => alert(data);
// Anonymous inline modules continue to work and can be postMessage()’d
worker.postMessage(module { export function fn() { return "hello!" } });

The syntax is reminiscent of const f = function() { ... } vs function f() {...}, and I kinda like that.

At the same time, this would allow the proposal to solve the bundler use-case. The counter-example from the Module Blocks README would be handled as follows:

module countBlock {
  let i = 0;

  export function count() {
    i++;
    return i;
  }
};

module uppercaseBlock {
  export function uppercase(string) {
    return string.toUpperCase();
  }
};

module combinedBlock {
  import {count} from countBlock;
  import {uppercase} from uppercaseBlock;

  console.log(count()); // 1
  console.log(uppercase("daniel")); // "DANIEL"
};

I’d suggest we discuss this possible merge at the incubator call, potentially under a new-ish name to avoid confusion (“Inline Modules”?) If it makes sense, we can move forward on the change as follows:

  • Below, in this thread (and in the incubator call, and possibly in plenary), talk through various semantic and syntactic details.
  • Make a PR against the readme to describe the combined proposal.
  • Write a specification for the combined proposal.
  • Bring the combined proposal to TC39 plenary to ask for consensus on a renewed Stage 2 on the whole package.

@littledan
Copy link
Member

littledan commented Jul 1, 2021

I think we should go in the direction identified by this issue, with these these major changes:

  • Module fragments are named by identifiers, not strings, so they are declared like module foo { export x = 1 }
  • Import statements can load a module fragment with syntax like import { x } from foo;, similarly as an identifier.
  • Import statements which import from a module fragment can work on anything which was declared by a top-level module fragment declaration in the same module, or one which was imported from another module. There's a link-time data structure representing the subset of the lexical scope which is the statically visible module fragments.
  • When a declared module fragment is referenced as a variable, in a normal expression context, it evaluates to a module block (one per time when it was evaluated, so the same one is reused for module fragments declared at the top level). It appears as a const declaration (so the link-time and run-time semantics always correspond).
  • Module fragments are only visible from outside the module by importing the containing module, and here, only if they are explicitly exported. They have no particular URL (note related issue: Portability concerns of non-string specifiers & import maps integration #10)
  • Module fragment declarations can appear anywhere a statement can, e.g., eval, nested blocks, etc (but they can only have a static import against them if they are at the top-level of a module). In contexts which are not the top level of a module, module fragments are just useful for their runtime behavior, of a nice way of declaring a module block.

We discussed this direction in an incubator call as well as a TC39 tools call, and feedback seemed generally positive. I'd like to follow up with a discussion in the July 2021 TC39 meeting. In my opinion, it's not necessary to merge this proposal with module blocks--it layers on top of module blocks cleanly already.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants