proposal-built-in-modules icon indicating copy to clipboard operation
proposal-built-in-modules copied to clipboard

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

Open littledan opened this issue 6 years ago • 111 comments

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 .

littledan avatar Dec 17 '18 17:12 littledan

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

Mouvedia avatar Dec 17 '18 17:12 Mouvedia

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 avatar Dec 17 '18 17:12 glen-84

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

glen-84 avatar Dec 17 '18 17:12 glen-84

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;

devsnek avatar Dec 17 '18 17:12 devsnek

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...

LinusU avatar Dec 17 '18 18:12 LinusU

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.

Mouvedia avatar Dec 17 '18 18:12 Mouvedia

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

LinusU avatar Dec 17 '18 18:12 LinusU

  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.

glen-84 avatar Dec 17 '18 18:12 glen-84

Another approach might be to use a new keyword like:

import Date builtin "date"

or

built-in import Crypto as c

martinheidegger avatar Dec 17 '18 22:12 martinheidegger

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.

gibson042 avatar Dec 17 '18 22:12 gibson042

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.

mattijs avatar Dec 18 '18 21:12 mattijs

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.

zenparsing avatar Dec 19 '18 00:12 zenparsing

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

ljharb avatar Dec 19 '18 00:12 ljharb

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) …

Mouvedia avatar Dec 19 '18 02:12 Mouvedia

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.

isaacs avatar Dec 19 '18 03:12 isaacs

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 avatar Dec 19 '18 04:12 thysultan

@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 avatar Dec 19 '18 05:12 zenparsing

@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

thysultan avatar Dec 19 '18 06:12 thysultan

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')

martinheidegger avatar Dec 19 '18 08:12 martinheidegger

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.

obedm503 avatar Dec 19 '18 17:12 obedm503

:: is used in E4X and JScript.

Mouvedia avatar Dec 19 '18 18:12 Mouvedia

@Mouvedia can you provide some more context?

obedm503 avatar Dec 19 '18 18:12 obedm503

@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.

Mouvedia avatar Dec 19 '18 19:12 Mouvedia

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). https://github.com/tc39/proposal-javascript-standard-library/issues/20#issuecomment-447931370
  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.

glen-84 avatar Dec 19 '18 19:12 glen-84

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

Mouvedia avatar Dec 19 '18 19:12 Mouvedia

why would you ever need to dynamically import standard modules?

devsnek avatar Dec 19 '18 19:12 devsnek

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

littledan avatar Dec 19 '18 19:12 littledan

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 avatar Dec 19 '18 19:12 glen-84

@glen-84 that's interesting to me it looks almost like a tag function.

e.g. std`foo`

Mouvedia avatar Dec 19 '18 19:12 Mouvedia

@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.

glen-84 avatar Dec 19 '18 19:12 glen-84