Open littledan opened 5 years ago
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 timeAnother 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::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.
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:
:
the std scheme will have to be registered"@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.
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
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:
:
/::
/etc. for a namespace, or /
for "sub-modules", as it can then conflict with other syntax (either current or future).import(std:crypto);
... is std
a named argument? Is crypto
an alias?@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.
@littledan maybe I missed this somewhere, but can dynamic import be used outside of type="module"
?
@obedm503 it also can be used in Scripts.
Babel style bare modules:
import {document} from 'Window'
Inconjuction with inline modules might still pave a happy path for polyfilling modules.
module Window {...}
@glen-84 Using template strings could on the other hand be used as pro-argument: Every built-in library has to be registered as template string in the vm & at build of the vm all the elements are registered the same way. What I like about the template string syntax is that it teaches that there are template strings and how those work.
I think scopes are necessary because new globals are currently being introduced to add new APIs. If we make those APIs modules, we would have potential naming conflicts. Adding scopes removes this limitation.
What if stdlib was not specified as a string but as a Symbol instead? And we could have a template to return them, e.g. import { Map } from std `@js/collections/immutable`;
(great idea BTW)?
Where
const std = module => isValid(module) ?
Symbol.get(module) : throw Error()
(or whatever syntax for error expression wins)
We could have imports "from symbol" reserved to native modules and have it easily versioned + npm independent:
import { api2020 as std } from std `@js/api`;
import { Map } from std `@js/collections/mutable`; // uses ES2020
import { X } from Symbol; // throws error
I'm trying to think of a brand new imports mechanism that would build on top of what we have and look kinda familiar.
Also, I personally don't feel satisfied with the idea of ::
or .
separators, because as much as I like C++/Java/Scala/whatever, they are not JS and their syntax doesn't have to be copied. That is, we should get inspired by possibilities of other languages but build on top of what we have instead of having hard copy. Many devs know import { Component } from '@angular/core
or similar, and as the pattern already had built its neuron paths in devs brains, we should extend that, not build another parallel highway.
@wopian @YurySolovyov @b-strauss The more I think about
import X from std`x`
the more I like the solution, I am curious why you don't/didn't like it.
That requires std
to be in scope, and be a function that’s normally callable - if it’s going to be consistent with existing tagged template literal syntax. Since imports are parsed before code is evaluated, i don’t see how that’s a viable path forward.
@ljharb I see and I kinda get your point: It would pollute the global namespace with a nodejs
and/orstd
function. But that function could work:
const modules = {
fs: Symbol('fs')
}
function std(strings) {
if (strings.length > 1) {
throw new Error('You can not request a module with a paramter')
}
const symbol = modules[strings[0]]
if (!symbol) {
throw new Error(`Unknown nodejs module: ${symbol}`)
}
return symbol
}
console.log(std`fs`)
Alternatively, you could simply treat the import ...
statements as "non-standard-blocks-of-code" .... which they already are, after all: you can not do any string operations in there: import x from "abc"+"def"
and any template string takes the template operation from a different API.
Alternatively one could simply namespace, the modules:
import fs from builtin.nodejs`fs`
where builtin
is in the global scope.
@martinheidegger to clarify, it simply wouldn't work because all imports must be resolved, per spec, before the global namespace (and any variables) are available in the first place.
@ljharb If you mean by this that it needs to be added to the es-module specification, then I totally agree.
@martinheidegger Even if it was added to the es-module spec, the simple fact is that no JavaScript code is evaluated before the modules are resolved.
This isn't like CommonJS where you can do require(someFunction("foo"))
, that simply is not possible with ES6 modules (unless you use the import(...)
syntax).
So even if a std
function existed in the spec, it wouldn't be capable of running.
Changing ES6 modules to allow for JS evaluation before resolving would be a huge change, with a lot of ramifications.
@pauan I didn't suggest that the es6 modules allow to execute arbitrary code. I am suggesting that template strings allowed to be used and executed are narrowly defined, but those narrowly defined functions will stay exposed to the general javascript vm for educational purposes (and for dynamic imports)
I think it would be massively confusing if tagged template literals worked magically differently in import statements than anywhere else - that seems like a nonstarter to me.
@martinheidegger So you are suggesting that only template literals are allowed, and the template literal function is hardcoded (i.e. it can only be std
)? And it does not use JS evaluation?
That would technically work, but what is the advantage of that over "@std/foo"
, "std:foo"
, or "std::foo"
?
@pauan the advantage is that "@std/foo" or "std:foo" is a namespace that suggest the same lookup operation as is used with any other module, which is not true for those modules.
@martinheidegger That is true, but there is precedent for that: require("fs")
uses a different lookup strategy from require("foo")
, since "fs"
is a built-in module.
And since std::foo
is a new syntax, that also suggests a different lookup strategy.
Given that import maps and loaders would let you override any of these lookups, it is, in fact, the same lookup operation as is used with any other module, full stop.
@ljharb so you don't have to explain why require('fs')
works just like that but that one needs to do something for require('express')
to work? (... there is a difference)
@pauan
Yes: there is the precedent of fs
but it is a rather bad example as it pollutes the module namespace (There is a Node.js discussion ongoing about putting new modules in a namespace as well).
Yes: it is a new syntax and alternatives would be import X from std::something
(no strings) or import X from Std\...
(also no string quotes) mentioned above. As long as the string-quotes are there, there is the explanation requirement as to why they work differently.
@martinheidegger I don't think it is necessary to remove the quotes.
Importing "http://foo.com"
is very different from "@foo/bar"
, which in turn is very different from "foo"
, which in turn is very different from "./foo"
.
Even though they all use strings, they use different syntax, and they all use different lookup strategies. So it is with "std:foo"
or "std::foo"
as well.
There is no "explanation requirement", since import strings are URLs, so having the std:
protocol automatically makes it different (just like how having the http:
protocol automatically makes it different).
Ideas I've heard so far:
What else are you thinking of?
Splitting into another thread based on https://github.com/tc39/proposal-javascript-standard-library/issues/12#issuecomment-447925779 .