proposal-built-in-modules
proposal-built-in-modules copied to clipboard
If we don't go with npm-style module specifiers, what should the syntax be?
Ideas I've heard so far:
- "scope:module"
- "scope::module"
What else are you thinking of?
Splitting into another thread based on https://github.com/tc39/proposal-javascript-standard-library/issues/12#issuecomment-447925779 .
There's also obedm503's proposal of simply not using strings. The scope would be implied in that case.
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?
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
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;
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...
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.
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 foreverhttps://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
- It's too verbose IMO, easy to introduce typos (even with copy/paste) without specialized linting, etc.
- 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.
Another approach might be to use a new keyword like:
import Date builtin "date"
or
built-in import Crypto as c
Copied from https://github.com/tc39/proposal-javascript-standard-library/issues/12#issuecomment-448025757 :
scheme::moduleis 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:moduleis still a valid relative URI reference, and even:scheme:moduleor%scheme:moduleor<scheme:moduleor|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.
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.
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.
If that's the case, then why wouldn't "the name between the @ and the /" provide the same amount of distinguishing?
Here's my summary:
- needs to be easily identifiable (amongst other imports)
- gotta be updatable/versioned (hopefully it will be perfect)
- should enable destructuring of std itself (not a feature for browsers but probably for embedding)
- if the separator chosen is
:the std scheme will have to be registered - gotta be convenient (e.g. std instead of standard is a convention)
- 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) …
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
- It will be easily identifiable, because no one would choose to make their userland libs look like this.
- A version number can be placed anywhere as needed for versioning.
- No url scheme to register
- No chance of being confused with a file path (except perhaps on Windows) or url
- Successful established precedent with PHP namespaces.
- New syntax can have new semantics (1js), and this is not valid JS today.
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.
@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).
@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
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')
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.
:: is used in E4X and JScript.
@Mouvedia can you provide some more context?
@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.
Unquoted module names are not ideal IMO:
- It's not easy to differentiate between a quoted and unquoted module with the same name (f.e. an npm package). https://github.com/tc39/proposal-javascript-standard-library/issues/20#issuecomment-447931370
- 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). - It wouldn't work well with dynamic imports (?) ...
import(std:crypto);... isstda named argument? Iscryptoan alias? - It's inconsistent with existing imports that use a string.
@glen-84 3. seems like a deal breaker to me.
why would you ever need to dynamically import standard modules?
@devsnek One common case would be using a standard module from a script.
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.
@glen-84 that's interesting to me it looks almost like a tag function.
e.g. std`foo`
@Mouvedia,
@martinheidegger suggested that above, but I think it's best not to use existing syntax in this position, as it could lead to confusion ... "is that a tagged template literal?", "can I create my own tagged template function for imports?", etc.