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

If we don't go with npm-style module specifiers, what should the syntax be? #20

Open
littledan opened this issue Dec 17, 2018 · 111 comments

Comments

@littledan
Copy link
Member

Ideas I've heard so far:

  • "scope:module"
  • "scope::module"

What else are you thinking of?

Splitting into another thread based on #12 (comment) .

@Mouvedia
Copy link

Mouvedia commented Dec 17, 2018

There's also obedm503's proposal of simply not using strings. The scope would be implied in that case.

@glen-84
Copy link

glen-84 commented Dec 17, 2018

This would then be valid:

import x from temporal;
import y from 'temporal';     // an npm package, for example

The difference is very subtle, and could lead to confusion/errors ... and possibly security concerns?

@glen-84
Copy link

glen-84 commented Dec 17, 2018

Staying with strings, there aren't many options.

Here are two alternatives. Not sure if either of them are "ideal", but perhaps worth mentioning:

import {a} from "std(date)"; 
import {b} from "std(date/sub)";
import {c} from "@some/thing";     // npm

import {a} from "std|date"; 
import {b} from "std|date/sub";
import {c} from "@some/thing";     // npm

@devsnek
Copy link
Member

devsnek commented Dec 17, 2018

i'm a fan of import { Instant } from time;. i can't really even imagine a case where we need more than a single identifier there.

if we wanted to really make things distinct i'd offer up import { Instant } from standard.time;

@LinusU
Copy link

LinusU commented Dec 17, 2018

How about just importing from a known URL, then we could also serve those modules over standard http and instantly be backward-compatible with all current runtimes.

import { Instant } from 'https://ecma-international.org/tc39/stdlib/time.js'

This is how Deno does it, which means that the stdlib would also be instantly usable from Deno.

Then runtimes can ship the stdlib preloaded right into the browsers if they want to...

@Mouvedia
Copy link

Mouvedia commented Dec 17, 2018

How about just importing from a known URL

If the modules fetched from reserved domains are automatically cached by the browsers with a long TTL, I would agree.
If not I think it's a bad idea.

@LinusU
Copy link

LinusU commented Dec 17, 2018

Since versioning (#17) isn't decided yet, it's hard to give an exact proposal but it should absolutely be cached.

In fact, if the browsers wanted to, they could ship their own custom implementation of the stdlib that gets loaded when a load from https://ecma-international.org/tc39/stdlib is detected.


If we are going to use SemVer, I'm suggesting that full version URLs would use Cache-Control: immutable headers so that they would be cached forever, and that semver-specifier would simply 3xx-redirect with an appropriate cache timeout:

  • https://ecma-international.org/tc39/stdlib/1.5.4/time.js -> returns file, cache this forever
  • https://ecma-international.org/tc39/stdlib/1.x/time.js -> redirects to latest (e.g. 1.5.4), cache the redirect for an appropriate amount of time

@glen-84
Copy link

glen-84 commented Dec 17, 2018

  1. It's too verbose IMO, easy to introduce typos (even with copy/paste) without specialized linting, etc.
  2. Including a version number in the module name would require you to update that name in many files when switching versions. IMO, this should be done with a mapping. i.e. a module name written as "example" maps to "[email protected]" when loaded. In that way, you only need to change the version number in one place.

@martinheidegger
Copy link

Another approach might be to use a new keyword like:

import Date builtin "date"

or

built-in import Crypto as c

@gibson042
Copy link

Copied from #12 (comment) :

scheme::module is a valid absolute URI, cf. RFC 3986 section 3 and section 3.3 and observe that hier-part (which follows the first ":") can be path-rootless, which starts with segment-nz, which starts with pchar, which can be ":".

URI scheme must start with an alphabetic character and consist only of alphanumerics, "+", "-", and "." (cf. section 3.1), but e.g. @scheme:module is still a valid relative URI reference, and even :scheme:module or %scheme:module or <scheme:module or |scheme:module (although not valid per RFC 3986) are accepted by the WHATWG parsing algorithm—but at least all but the first of those are nonconforming and therefore at least potentially subject to special treatment (likewise for any other format that starts with something other than an ASCII letter, "/", "?", "#", or a URL code point).

If the idea is to differentiate standard library modules from potentially-relative URIs, I think a super-special prefix like "%" would be most prudent.

@mattijs
Copy link
Member

mattijs commented Dec 18, 2018

I would vote for scheme::module. I think we should stay with strings to avoid confusion (and mistakes?) and complicating the import syntax. To me the :: is a strong enough signal that the module is distinct from other (user land) modules.

Something that looks like an http/https URL does not make sense in all contexts you can find an import statement in and they are also easily mistaken for user modules.

@zenparsing
Copy link
Member

I'm not sure I understand how :: will help. I think that from the user's (and readers) point of view, the distinguishing feature won't be the number of colons, but rather the name before the colon.

@ljharb
Copy link
Member

ljharb commented Dec 19, 2018

If that's the case, then why wouldn't "the name between the @ and the /" provide the same amount of distinguishing?

@Mouvedia
Copy link

Mouvedia commented Dec 19, 2018

Here's my summary:

  1. needs to be easily identifiable (amongst other imports)
  2. gotta be updatable/versioned (hopefully it will be perfect)
  3. should enable destructuring of std itself (not a feature for browsers but probably for embedding)
  4. if the separator chosen is : the std scheme will have to be registered
  5. gotta be convenient (e.g. std instead of standard is a convention)
  6. some kind of reflection (e.g. some way to tell if the browser has that module in std, to fallback)

"@std/foo" cannot be considered if you agree with 1.
Using URLs directly reminds me of DOCTYPE and will probably facilitate sloppiness and versioning management on the client side. It should be transparent for the developer: the browser should handle the hotfixes/errata.
If tree shaking (of the std itself) is really essential for embedding purposes then you gotta keep an explicit name for std: it cannot be implied. For the ones wondering what I mean by 3. you gotta imagine a hypothetic standard library that would be at least as big as lodash/underscore.
6. is important so that we can dynamically import the polyfill if the browser is outdated. IMHO importing a missing module from std shouldn't fail but return undefined to enable if (!defaultImport) …

@isaacs
Copy link

isaacs commented Dec 19, 2018

None of this makes the case for why anything else is better than @std/foo, as none of it addresses the cowpath-paving consideration. I like the idea of dropping the string entirely, and just putting a bare identifier in the import statement. But again, people will copy this and then get annoyed that they can't do it natively in platforms that support it for std modules. Why not just do it the easy way people already understand?

But all that being said, I'd like to earnestly suggest:

import now from Std\1.5.4\CoreLibs\Temporality\Time\Date
  1. It will be easily identifiable, because no one would choose to make their userland libs look like this.
  2. A version number can be placed anywhere as needed for versioning.
  3. No url scheme to register
  4. No chance of being confused with a file path (except perhaps on Windows) or url
  5. Successful established precedent with PHP namespaces.
  6. New syntax can have new semantics (1js), and this is not valid JS today.

@thysultan
Copy link

thysultan commented Dec 19, 2018

In favour if non-string syntax for among other reasons that it feels the most "standard" or as part of the "environment" and not something akin to an external import.

For example assuming a host(the browser) exposes the Window import. It could be written as either of the following:

import document from '@std/window'
import document from 'std:window'
import document from Window

As aforementioned the first case very much conflicts negatively with npms "@Scoped packages by means of imitation(it looks like a scoped npm package, but actually isn't), i would imagine avoiding this would be in the over arching proposals best interest similar to the case with pattern matching and an overloaded switch syntax.

The second form is, much like the first, "a magic string" that we have come to associate(implicitly/explicitly) with external packages.

This form also shares some semblance to data url imports: import x from "data:text/javascript,\...".

The third conforms closer to how we already implicitly reference "standard" objects. Object, Number etc.

I can't hope but wonder if this would also play well with inline modules making it easier to polyfil future/current standard library functions that are "not-yet" exposed.

@zenparsing
Copy link
Member

@thysultan Wouldn't the importing of bulit-in modules using identifiers actually conflict with possible future inline modules?

module Window { export const x = 1 }
import { x } from Window; // lexical Window or "built-in" Window?

Note that Dart uses scheme-like specifiers (dart: for builtins and package: for userland).

@thysultan
Copy link

thysultan commented Dec 19, 2018

@zenparsing That's what makes it easier to polyfill without the need for a built-time transpiler.

I was using the term polyfill in both the "imitate future features" and "fix current features/bugs" context, given there are some polyfill that only do the later.

Though we might hold engine implementations to a high standard, there are times when they might be broken or are lacking in the levels of abstraction/features one might desire, introducing the need for an author to extend it to fit the bill. For example:

module Window extends Window {
// extend the Window module
export const head = document.head
}

module Window {
// implement a Window polyfill
export const document = window.document
}

This lays a good foundation for the consumer to never have to worry about changing import identifiers between using the builtin, or polyfill for/if any of the aforementioned reasons arise.

import {head} from Window

@martinheidegger
Copy link

martinheidegger commented Dec 19, 2018

By thinking about backwards compatibility what about:

import crypto from std`crypto`

this way it could work in node like

import fs from node`fs`

or in require form

const fs = require(nodejs`fs`)
// ... or ...
const fs = require.builtin('nodejs', 'fs')

@obedm503
Copy link

based off the idea of namespaces and the idea of not using strings/URL, what about?

import crypto from std::crypto

// and in node .mjs
import fs from node::fs

again, node/commonjs can do it's own thing be it require.builtin or require('std::crypto'). I'm thinking of this proposal as an extension to the ES module system.

@Mouvedia
Copy link

:: is used in E4X and JScript.

@obedm503
Copy link

@Mouvedia can you provide some more context?

@Mouvedia
Copy link

Mouvedia commented Dec 19, 2018

@obedm503 I am just saying that :: may already have an ingrained meaning if you used the E4X extension or the JScript dialect. More recently you had the bind operator proposal.

@glen-84
Copy link

glen-84 commented Dec 19, 2018

Unquoted module names are not ideal IMO:

  1. It's not easy to differentiate between a quoted and unquoted module with the same name (f.e. an npm package). If we don't go with npm-style module specifiers, what should the syntax be? #20 (comment)
  2. It's difficult to introduce punctuation like :/::/etc. for a namespace, or / for "sub-modules", as it can then conflict with other syntax (either current or future).
  3. It wouldn't work well with dynamic imports (?) ... import(std:crypto); ... is std a named argument? Is crypto an alias?
  4. It's inconsistent with existing imports that use a string.

@Mouvedia
Copy link

Mouvedia commented Dec 19, 2018

@glen-84 3. seems like a deal breaker to me.

@devsnek
Copy link
Member

devsnek commented Dec 19, 2018

why would you ever need to dynamically import standard modules?

@littledan
Copy link
Member Author

@devsnek One common case would be using a standard module from a script.

@glen-84
Copy link

glen-84 commented Dec 19, 2018

Could the import syntax be extended to allow runtimes to use a keyword after from?

import crypto from std "crypto";
import crypto from nodejs "crypto";

The dynamic loading syntax would be slightly more exotic:

const crypto = await import(std "crypto");
const crypto = await import(nodejs "crypto");

Just thinking out aloud.

@Pauan
Copy link

Pauan commented Mar 19, 2019

@lazarljubenovic To be clear, there is no "invention" of a new mini-language or syntax. Imports in JS already use URLs, and the foo:bar syntax is a part of URLs.

Yes, that does mean that import * as foo from "http://someurl.com" is valid, and already works. That's just how imports in JS are.

So whether you like it or not, imports in JS already carry additional semantic information. So there are no changes or additions to the language. It's just about deciding how to use the already existing URL system.

@ljharb
Copy link
Member

ljharb commented Mar 19, 2019

@Pauan no, imports in js use specifiers. Browsers have chosen (as the spec allows them to do) to treat specifiers as URLs. The difference is important - browsers aren’t the entirety of what matters.

@Mouvedia
Copy link

As Iv pointed out several times, we cannot make an educated decision if we don't clarify whether we plan to have:

  1. only one unique scope (probably named std)
  2. one current scope (which name MAY change depending on the platform)
  3. several scopes that can coexist (and one shared by all platforms)
  4. several scopes but no compulsory requirement on a name for the "standard" library

Once the possibility/enablement/authorization is acknowledged—or not, we will be able to pick the right proposal for the job. Should we vote on this? Has this already been decided? Should the TC39 officially weight in on the issue?

@Pauan
Copy link

Pauan commented Mar 19, 2019

@ljharb Node also treats them as URLs: import * as foo from "file://path/to/file.js"

This was intentionally done to match with browsers, to avoid fragmenting the ecosystem.

In addition, even if it was browser-specific, that's irrelevant. @lazarljubenovic 's point was that he didn't want a new syntax added to strings.

And my point is that the "new" syntax (i.e. URLs) has already been added. That's still true, even if it was browser-only (which it isn't).

@riggs
Copy link

riggs commented Mar 19, 2019

Hello, I'm an outsider to this community and process who just saw Chrome's blog post. After reading through a fair bit of the issue threads about the namespace, I'm left wondering why I haven't seen any discussion of simply introducing a new global object, std?

@obedm503
Copy link

@riggs I believe polluting the global is exactly why an import-based standard library was suggested.

@isaacs
Copy link

isaacs commented Mar 19, 2019

Chrome shipping an experimental API is far from being a second cowpath. It's an experiment. @-prefixed namespaces for JavaScript modules has been in use for years by the platform used by something like 93% of all JavaScript users.

A "cowpath" is not just "an existing implemented way to do a thing". It's the path that is beaten down by people wandering about without a particular direction or intent, who all happen to land on a given convention or pattern, and then settle on that pattern because doing things mostly the same thing as everyone else is valuable in a community.

Chrome shipping an experimental API is... well, I'm not sure what the metaphorical equivalent would be, but it's about as close to the exact opposite of a cowpath that could exist.

It might attract us cows, but that seems unlikely, especially in the short term. It's not something that's supported by other browsers (yet?) and since it's experimental, it's not something you can rely on anyone having, or even existing for the long term.

@Mouvedia
Copy link

Mouvedia commented Mar 20, 2019

@isaacs The thing that you don't seem to understand is that Microsoft mostly gave up on Chakra and switched to Blink for Edge—like Opera before. Chromium is dominant.

Now check this page; out of the 21 members listed you have 10 that work for a company using webkit or blink (6 google, 2 apple, 2 microsoft) and only 3 working for Mozilla. This list isn't even up-to-date, it's probably way more now.

What Google picks matters. And you will have a hard time reverting what they will eventually choose. It might even be too late…

@devsnek
Copy link
Member

devsnek commented Mar 20, 2019

Now check this page

that page isn't anywhere near accurate, if you go to the github repo it was last updated 7 years ago.

aside from that, google doesn't own tc39. please stop assuming otherwise.

@martinheidegger
Copy link

On other notes: I can't see this topic on the next TC39 agenda:
https://github.com/tc39/agendas/blob/master/2019/03.md

@Mouvedia
Copy link

@devsnek please don't distort what I am saying.
TL;DR: Mozilla is a minority in the TC39 and browser market share wise as well. Google is the dominant player like Microsoft used to be.

Now here's the part which is subjective: whatever Google will pick will be the standard.

@riggs
Copy link

riggs commented Mar 20, 2019

@riggs I believe polluting the global is exactly why an import-based standard library was suggested.

Does a single additional global really count pollution?

It handily side-steps the whole import namespace problem, and can readily reuse extremely common existing patterns for compatibility.

@obedm503
Copy link

@riggs

Does a single additional global really count pollution?

perhaps you're right, but either way, there is already a global to which all of the current standard library is attached: window (at least on the browser).

For a common Global across all environments, there is already a proposal
https://github.com/tc39/proposal-global/


From my perspective, builtin modules give us a chance to "fix" issues in the current standard. Like the new Temporal proposal, or @glen-84's suggestion:

import {Base64} from core "encoding";

Base64.encode("Hello, world");

as replacements for Date, and btoa and atob.

And while new we could add new Globals like Temporal and Base64, I find imports more aesthetically pleasing; they make JS feel more mature. ¯\_(ツ)_/¯

Disclaimer: There are some better and more logical arguments, this is just my take.

@Pauan
Copy link

Pauan commented Mar 20, 2019

@-prefixed namespaces for JavaScript modules has been in use for years by the platform used by something like 93% of all JavaScript users.

Citation needed. Is npm big? Yes, definitely. 93%? Highly unlikely.

And in any case, a solution requires the consensus of browsers. So if Chrome has decided on one path, then that carries a lot of weight, regardless of how big or not big npm is.

@riggs
Copy link

riggs commented Mar 20, 2019

@obedm503 I think globalThis has value and seems distinct from this discussion.

I'm mostly curious as to why I haven't seen discussion of, e.g.:

stdLib.Base64.encode("Hello, world");

Edit: Or std.lib / std.web / etc.

Namespaces in the import path for stdlib stuff just seems needlessly complicated.

@littledan
Copy link
Member Author

Google's been doing great work in TC39, and V8 achieves a high level of spec-compliance. I'm confident that we can work towards a common conclusion here through standards.

@azz
Copy link

azz commented Mar 20, 2019

Having a subglobal God object hanging off of window/global seems like a poor path to me. Isn't the point of imports to only load code that an application is using, and make code easier to statically analyse?

@isaacs
Copy link

isaacs commented Mar 20, 2019

Yes, Google is the most dominant player in the browser market, and they swing a lot of weight, indeed quite a bit like Microsoft used to back in the day.

But that's still not a cowpath, is what I'm saying. It's a highway being paved by a dominant technocrat.

Which might be fine in a lot of cases, I mean, you don't want cows to run the show, and presumably the technocrats are smarter than cows about a lot of things.

The idea of "paving cowpaths" in language design is that, when the goal is to design language features that will satisfy a community and be easily adopted with a minimum of disruption, it's good to take stock of the patterns that have naturally emerged.

Google putting something in an experimental release is not "paving a cowpath", because in this context, Google is not the cows.

Citation needed. Is npm big? Yes, definitely. 93%? Highly unlikely.

Well, "big" is subjective.

I can say that more than 11 million people use npm. This number is leveling out as we reach saturation of all JavaScript developers worldwide, and is now growing merely at the rate of new JavaScript developers entering the field or switching from other languages. This is about 50% of all developers worldwide (on the order of 15-30M, depending on what you count as a "developer"). (A few years ago, it was much more of a hockey-stick.)

The 93% number is based on a survey done by the JS foundation, Node.js foundation, and npm, Inc., of a worldwide sample of JavaScript (and non-JavaScript, but JavaScript-using) developers around the world, spanning an impressive number of industries.

So, I don't know if that's "big", but according to the best data anyone's been able to gather, it is about 93% of all JavaScript users using npm. Of course, that doesn't mean that 93% of JS devs are publishing packages to npm. We've only got a million packages or so, in ~8M versions. But they're accessing the registry to do their JS work, so they're being exposed to the patterns of module naming conventions, and getting along fine with them.

I'll leave the googling as an exercise for the reader. Start with IDC's reports on the number of devs worldwide, the JS ecosystem survey from 2018, and the great work that Laurie Voss has presented again and again, correlating the survey data to npm's internal data. Or just go find 100 JS devs, and let me know if more than 7 of them never touch npm.

But this isn't about npm. This is about the namespacing pattern that people are using. npm didn't really even "invent" that so much as choose it by a process of elimination, and I don't think we've ever claimed to own it. You can use @foo/bar in a node app without ever touching npm, or (with a bit of webpack and babel) in any browser app.

The point is that this is the cowpath, so why not pave it? Chrome is probably gonna implement the standard, because they trust TC-39 to wisely design a standard that serves the needs of JS developers, but they'll also push on it to make things move. So why not make the standard be the thing that people apparently are cool with using, and have been using for years? Why redesign this bikeshed?

@riggs
Copy link

riggs commented Mar 20, 2019

Thinking about this more, the ergonomics of using what was presented in Chrome's blog are pretty poor. Needing to rely on a special tag injected into my HTML to handle polyfills is a burden I don't currently have.

If someone were to make a std npm package that tries to import everything, catches the exceptions, includes the polyfills, and hangs it all off of a global object, I'd use it in a heartbeat and never look back. I'll trust the JS engine to optimize away unneeded code, as it's likely already doing (doubly-so since this code will be part of the engine, anyway).

@YurySolovyov
Copy link

The idea of "paving cowpaths" in language design is that, when the goal is to design language features that will satisfy a community and be easily adopted with a minimum of disruption, it's good to take stock of the patterns that have naturally emerged.

They emerged due to lack of standard namespacing mechanism, so if we were to actually introduce one, why look back at half-measures people came up with?

@azz
Copy link

azz commented Mar 20, 2019

This is discussed a lot in #12, but can we summarize the for/against "pave the cowpath" discussion?

Pros

  • Language doesn't need to change.

  • Don't need to register a/many URI schemes.

  • Testing tools should "just work" for the most part, e.g. module mocks in Jest.

  • Polyfills could potentially be published on npm with the same name, e.g. @web/kv-storage. Your bundler could then intelligently generate import maps without additional configuration.

  • TypeScript @types/ packages will "just work", if published as, e.g.,
    @web/kv-storage or @types/web__kv-storage.

  • Will work with dynamic import()

  • ❓Anything else

Cons

  • Tooling would need to recognise a subset of "npm scopes" (@std et al) as "builtin" scopes, and not attempt to bundle them, resolve them, etc. It would be cleaner if there was a way to distinguish builtins.

  • Educating the difference between builtins and npm modules.

    I have seen countless times beginners to Node.js and npm install builtin modules like fs, path in their package.json files. It's not obvious without looking up the documentation that these are builtin modules.

    However, if the Node.js builtins were prefixed with @nodejs/ this problem would go away. npm install @nodejs/fs would fail because you can't squat the name. So if @std/, @web/, or @builtin are chosen I don't see this as huge problem.

  • ❓Anything else

@Pauan
Copy link

Pauan commented Mar 20, 2019

Language doesn't need to change.

To be clear, the language doesn't need to change either way, since URLs are already used for JS imports (in both browsers and Node).

Don't need to register a/many URI schemes.

Chrome went ahead and added in std: without registering it, so I'm not sure if that's actually a problem in practice.


One more downside of @foo: it might conflict with existing URLs that start with @, whereas that cannot happen with foo: (since it's a different protocol).


The rest seems accurate. Thanks for posting a summary of the two positions!

@azz
Copy link

azz commented Mar 20, 2019

[...] the language doesn't need to change either way [...]

Not for std:, but some other suggestions in this issue are syntax changes.

@Pauan
Copy link

Pauan commented Mar 20, 2019

@azz Oh, well if we're talking about the new syntax proposals, then you should add a new downside: they don't work well with dynamic import().

So really there's three proposals: @foo, foo:, and the various new syntax proposals. So I think it should be split into three sections, not just Pros/Cons.

@obedm503
Copy link

Perhaps I'm missing something, but what is the practical use of dynamically importing a built-in module?

@glen-84
Copy link

glen-84 commented Mar 20, 2019

@obedm503 #20 (comment)

@SMotaal
Copy link

SMotaal commented Apr 30, 2019

@mattijs can you close the loop on this would relate to concurrent changes for stage 2?

I find it a little hard to know what input would be relevant/outdated.

@trotyl
Copy link

trotyl commented May 3, 2019

The term "JavaScript" does not even exist* in the spec, how could the abbr. js lives without corresponding full form explained?

*: Only exception in Introduction: ECMAScript is based on several originating technologies, the most well-known being JavaScript (Netscape) and JScript (Microsoft).

@glen-84
Copy link

glen-84 commented May 3, 2019

Also, as I mentioned before, js: is very generic – web: and nodejs: would also be "JavaScript".

I prefer something like core:, unless someone has other ideas?

(yes, this is slightly off topic, but it looks like they're going ahead with the x:y syntax?)

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

No branches or pull requests