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

Bikeshed possible syntaxes for import statements #6

Closed
littledan opened this issue Nov 7, 2019 · 14 comments
Closed

Bikeshed possible syntaxes for import statements #6

littledan opened this issue Nov 7, 2019 · 14 comments

Comments

@littledan
Copy link
Member

Various syntax alternatives are proposed in #3, #5 and WICG/webcomponents#839.

What do you think of these alternatives? Are there other syntaxes we should consider?

@bmeck
Copy link
Member

bmeck commented Nov 8, 2019

I'd personally prefer out of band and just wanted to be sure that is left considered.

@thw0rted
Copy link

Would this be the right place to consider syntaxes that don't use import at all, or should that be a different issue?

@littledan
Copy link
Member Author

It's pretty hard to introduce a new keyword into JS syntax-wise. What would be the motivation? (Here is a good place to discuss IMO)

@thw0rted
Copy link

I came to this repo from the webcomponents issue I linked above, because I'm interested in the implications these decisions will have on the scripting security model. The goal that I see here is to have a static import syntax that works more or less the same as Body#json() from a security perspective -- that is, I should have a way to (statically) load content, passed through a built-in parser, that is guaranteed not to have side effects.

I'm not an expert in the current state of the various standards involved, so I could be wrong, but my understanding is that the static import keyword is currently used to load an ES module. Now, in some environments, it can also mean to perform the same kind of loader logic against a file that isn't an ES module, then run it through some built-in parser. (I'm thinking specifically of import-from-JSON, and import-from-HTML, but maybe this applies to WebASM or maybe even a WebGL shader or model, anything that might be useful to a script and that has a built-in parser in at least one execution context.)

Am I right in thinking that this second "mode" isn't actually part of a common standard? At least when I use it in my own projects today, it winds up going through a transpiler / webpack loader under the hood. But the goal, as I understand it, is to formalize this behavior so that we can have consistency across environments, with first-class support that doesn't require polyfills or transpilers. That leaves us with two distinct security models: ES modules that load via a script execution engine with global state, and a bunch of module types that load via stateless parsers that handle "passive" content like JSON, HTML, etc.

Today's transpilers implement both of these features with the same keyword, but why should that be so? Sure, it's easy to piggy-back on import because it's already reserved, but in a very real sense we're overloading it to do two different things. Other issues here debate how best to ensure that the loader treats the file as the correct type, but from a security perspective I'd argue that the only distinction that matters is which of the two bins it should fall into.

So, that's my motivation. Strictly from the perspective of security-model concerns, using two different keywords solves it and neatly sidesteps basically every concern raised in this repo -- attribute syntax, in- vs out-of-band, handling in Node vs web, interactions with MIME types, etc. None of that is necessary if we just say that import is only for ES modules, and a different keyword would be used for "everything else".

@bmeck
Copy link
Member

bmeck commented Nov 14, 2019

@thw0rted all JS Spec defined modules are part of an Abstract Module Record type intended to allow interoperability with other Module types potentially not in the specification. The design was to allow a single system of loading a variety of types. In addition, a variety of uses are possible without side-effects but still going through a JS module of sorts such as:

  • Redirecting to a different module such as export * from './foo.json';
  • Defining/initializing functions without executing code on initial loading export function add(a, b) { return a + b; };

I'm unclear how a split would occur naturally for these differing types and it seems like they should be left to the same mechanism for loading. We could try and force an artificial separation of types, but that seems unclear in motivation and benefits to me currently.

@thw0rted
Copy link

I always thought of export * from "whatever" as being a shorthand for import {a,b,c...} from "whatever"; export {a,b,c};. Viewed that way, the export stays the same and the import would just change to the second keyword and behave as described in my previous comment. Of course that means that you'd need a second new keyword to pair with export. (As I said in the linked comment on the webcomponents issue, I'm loathe to suggest concrete keywords, but for a very poor example, let's say passiveimport and passiveexport.) This just reflects my personal experience with the syntax so I could be missing some nuance or use-case.

As for the second bullet, I'm aware that a given JS module could be written to have no side effects, but you can't reason about whether they will or not when importing them, because they're evaluated in the same global scope -- any JS module could have side effects, from the security model's POV. The "forced, artificial" separation of types is actually a natural division between evaluating a resource that has access to the global scope (i.e. a JS module) versus evaluating a resource that does not have that access (i.e. everything else).

@phistuck
Copy link

It looks weird to me that this proposal aims for a single attribute.
There are few attributes that are already needed -

  • Type
  • Nonce
  • Hash/integrity
    How about -
import {...} from {url: ..., type: ..., hash: ...}

This resembles fetch(URLStringOrRequestObject) and is forward looking.

@xtuc
Copy link
Member

xtuc commented May 11, 2020

@phistuck while the proposal promotes a single type attribute at the moment the goal is too have arbitrary attributes. I think that your syntax suggestion is interesting

@littledan
Copy link
Member Author

The current proposal supports arbitrary key-value syntax, as import { foo } from "module" with attr: "value", attr2: "value2". It defines constraints around the interpretation of type: "json" and type: in general. I think it is already extensible in the way @phistuck proposes, but the module specifier remains distinctly positioned.

@Jack-Works
Copy link
Member

Whatever syntax we choose, I think it is important to separate different kinds of attributes at syntax level. Otherwise, it will get harder to write import for different platforms. (See #30 (comment) #30 (comment) and #30 (comment) )

@littledan
Copy link
Member Author

I don't have any non-assert attributes to propose. What if we say that with sets off assert-type attributes, and some other keyword is added in a follow-on proposal to enable attributes which change module interpretation?

@viktor-yakubiv
Copy link

I went through #3, #5 and a few other related issues. I agree with various sayings but the most consequential to me are the following.

Since with is not recommended and forbidden in the strict mode (@littledan in #3 (comment), MDN), it would be confusing to introduce in this scope even if it has nothing related to the actual with statement. I assume that for an experienced software engineer with would be clear, however, a junior developer may be confused and afraid to use such syntax.

Going through various @chicoxyzzy's comments in #3 (first, second, third) I generally agree with @justinfagnani in #5 that something like the object notation would look more readable. However, I do not agree that the record syntax (comment) is a good idea in this case.

Trying to combine these arguments it took some ideas from C++ and Pascal and baked it into the syntax.

Dynamic import

I thought about 2 options given in the current README#Before stage 3 I vote for the short syntax.

If we define import() as a function with 2 parameters as the following

function import(moduleUrl, moduleAttributes) {
  ...
}

passing an object like { with: { ... } } does not look logical since the actual attributes are always under the single property value.

Also, defining with (or any other keyword) as a key might be confusing to a new coming developer who's just learned that certain words must be avoided for naming identifiers. Going further, some basic syntax highlighters always highlight with as a statement even if it's under the object notation what could make using a keyword as an object key even more confusing.

As a counter-argument to this may be wishing to add one more keyword to the import statement, i.e. for the following static import:

import package from './package.json' with type = 'json' having rel = 'fallback'

the dynamic call might look like:

const package = await import('./package.json', {
  with: { type: 'json' },
  having: { rel: 'fallback' },
})

However, this does seem to be too complicated and can be easily blended into the current proposal as an attribute:

const package = await import('./package.json', { type: 'json', rel: 'fallback' })

Currently, I do not see any possible need to add another keyword to the import statement, hence no need to use such keyword in the configurational object to pass attributes.

Conclusion and proposal

To conclude my arguments from above I propose the following example:

const GLOBAL_CONFIG_JSON_VERSION = 2
const GLOBAL_CONFIG_DEFAULT_ATTRIBUTES = {
  syntax: `json-${GLOBAL_CONFIG_JSON_VERSION}`,
}

import('./config.json', {
  ... GLOBAL_CONFIG_DEFAULT_ATTRIBUTES,
  type: 'json',
})

Since it's dynamic import, dynamic constructing of attributes should be allowed. I did not find any sayings about this, perhaps it's obvious.

Static import

For the static import, I propose the following syntax:

Shorthand syntax

Such syntax could be used when a single attribute is passed.

import package from './package.json' using type = 'json'

Full syntax

import package from './package.yml' using {
  type = 'json';
  syntax = 'YAML';
}

The object-like-literal may be confusing but this is not an object constructing but more a block scope.

Also, using is a new keyword what would no make it confusing with actual with statement. On the other hand, using is not a restricted keyword what may leak to backward compatibility issues as well as other options (having, given, when) like in the following example:

import given from './given'

given('Viktor Yakubiv') // should return 'Viktor'

the engine should possibly throw SyntaxError: Unexpected token 'given' when given becomes a reserved word. A similar example could be found for any of the proposed words.

Alternatives

In my example, the assignment operator may be as confusing as the object notation. Hence I propose a few alternatives:

Two colons (::)

Similarly to C++ namespace access. Perhaps looks weird with a string literal.

import package from './package.json' using type::'json'
import config from './config.yml' using {
  type:: 'json',
  syntax:: 'YAML',
}

Pascal asignment (:=)

Logically is good but looks a little bit unusual or even weird.

import package from './package.json' using type := 'json'
import config from './config.yml' using {
  type := 'json',
  syntax := 'YAML',
}

However, it can be generalised in way that only a static literal can go after such assignment, i.e. a clause const a := 'hello' can be optimised by an engine or a transpiler.

Singular arrow notation (->)

import package from './package.json' using type := 'json'
import config from './config.yml' using {
  type -> 'json',
  syntax -> 'YAML',
}

I am not sure it's not reserved yet by any other proposal or already rejected by this one. The code may look like PHP and the syntax could be confusing too.


Sorry, if this is too long and messy. I appreciate any feedback.

@xtuc
Copy link
Member

xtuc commented Jun 6, 2020

passing an object like { with: { ... } }

The Worker constructor already use a type key in an object as its second param, in that case we need to nest the attributes. I'd prefer that we keep APIs consistent and only pass the attributes in a nested object. We suggested to use with as a key to reflect the ImportDecleration syntax.

The syntax with operators wasn't mentioned before but it looks overloaded to me.

@nicolo-ribaudo
Copy link
Member

nicolo-ribaudo commented Mar 31, 2023

The proposal has settled on the import [bindings] from [source] with { [key1]: [string1], [key2]: [string2], ... } syntax, with the dynamic import equivalent being import([source], { with: { [key1]: [string1], [key2]: [string2], ... } }).

Thanks for contributing to the discussion!

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

No branches or pull requests

8 participants