Closed jonathantneal closed 1 year ago
We already have infinitely extensible syntax with no runtime semantics - comments. Adding syntax that does nothing at runtime is adding a lot of cost for decidedly non universal value.
Out of Topic: That's how I think of the Type Comment proposal
We already have infinitely extensible syntax with no runtime semantics - comments. Adding syntax that does nothing at runtime is adding a lot of cost for decidedly non universal value.
Out of Topic: That's how I think of the Type Comment proposal
By this logic, we can call JS a complete language. Who needs pipelines, pattern matching, or import reflection? We can just add comments to the appropriate places and let the build steps magic everything together.
@devongovett yes, that is the exact idea. Adding something to the language is a permanent decision that can never be unmade; “time to add” is, in the long run, a negligible cost compared to permanent mistakes. Moving too fast breaks things, which slows down everything that comes after it. That’s the job of JS language stewards imo - to minimize mistakes, not to accelerate experimentation (which requires no standards process anyways)
@jridgewell i don’t think your slippery slope there is accurate. What may be an accurate logical extension is, that anything without runtime semantics can and perhaps should be achieved without any alterations to the language. Anything with runtime semantics, however, is unrelated to that logic.
Not everything can or should be standardized by TC39. What if browsers want a feature that doesn't make sense in other runtimes? Or the reverse? Is JS going to standardize a feature of a single runtime? How about features of a bundler? I don't think the JS language should be playing gatekeeper.
We already have infinitely extensible syntax with no runtime semantics - comments. Adding syntax that does nothing at runtime is adding a lot of cost for decidedly non universal value.
Adding something to the language is a permanent decision that can never be unmade; “time to add” is, in the long run, a negligible cost compared to permanent mistakes. Moving too fast breaks things, which slows down everything that comes after it. That’s the job of JS language stewards imo - to minimize mistakes, not to accelerate experimentation (which requires no standards process anyways)
This comes across as dismissing community feedback because we're perceived not to fully appreciate the problem or problem space.
Is this a forum for discussion that is legitimately open to non-TC39 members of the community?
That first comment is such a facile argument; it doesn't contribute to the discussion and is unnecessarily dismissive. There is a major undertaking right now for exactly the sort of non-semantic syntax 'that can be accomplished in comments' before this very same group and with significant momentum.
I will bow out of this discussion.
As requested in the tools meeting today, here are some potential use cases for import attributes. Tried to group them into some kind of semantic categories.
// this proposal
import mod from './foo.wasm' with { type: 'module' };
// more reflections
import img from './foo.png' with { type: 'url' };
import img from './foo.png' with { type: 'arraybuffer' };
import img from './foo.png' with { type: 'dataURL' };
import img from './foo.png' with { type: 'arraybuffer' };
import worker from './worker.js' with { type: 'worker' };
import sw from './worker.js' with { type: 'service-worker' };
import svg from './foo.svg' with { type: 'dom' };
import svg from './foo.svg' with { type: 'image' };
// tc39/proposal-defer-import-eval
import {x} from "y" with { lazyInit: true };
// attributes to apply to a constructed object
import stylesheet from './foo.css' with { layer: 'utilities' }; // css cascade layers
import stylesheet from './foo.css' with { media: 'print' };
import audio from './file.mp3' with { loop: true };
// preloading (possibly related to lazyInit?)
import x from './foo.js' with { preload: true };
let mod = await x.load();
// more assertions
import x from 'y' with { assert: { integrity: '...' }};
import x from 'y' with { assert: { referrerPolicy: '...' }};
// transforms that bundlers/tools could implement
import img from './foo.png' with { width: 500 };
import img from './foo.png' with { convertTo: 'jpeg' };
I'm sure there are more as well (please leave a comment!), but hopefully this demonstrates the need for an extensible syntax.
@devongovett thanks! i'd love some more elaboration on what these do (in prose), as well as what they would be expected to do in browsers and node.
@devongovett the examples really do help a lot to try to think more concretely about the requirements here.
We also went through a lot of these cases when exploring the possibilities for import reflection, so I can share some feedback based on what came out of that process (mixed in with my own opinions of course!), see comments below:
import mod from './foo.wasm' with { type: 'module' };
This is explicitly a non-goal for the Wasm ESM integration. The content type determines the format, and that is fully set at this point. To be very clear, there will most likely never be a custom module type for Wasm, as it will work in the ESM integration without any custom attributes.import img from './foo.png' with { type: 'url' };
: The way to do this is via import.meta.resolve('./foo.png')
. This use case seems ideally is most likely captured by the asset references proposal as well.import img from './foo.png' with { type: 'arraybuffer' };
, import img from './foo.png' with { type: 'dataURL' };
: These are effectively decorated fetches, similar to fetch(import.meta.resolve('./foo.png'))
. If the benefit of moving these to syntax is being able to standardize on a static data method that tooling can inline etc I'm all for that. But that does require full standardization here I think to draw the benefits over direct fetching. Having this discussion as part of the asset references proposal seems to make sense to me. Similar concepts have also come up in the context of data imports for Wasm and there was even some mention of hope that alignment might work out for an asset type to support data segment imports somehow.import {x} from "y" with { lazyInit: true };
: This is being actively worked through. It may well align with import reflection yet.import x from './foo.js' with { preload: true };
: Lazy initialization would load all modules but not execute them. I think preload is identical from that perspective, so likely is the same?import worker from './worker.js' with { type: 'worker' };
, import sw from './worker.js' with { type: 'service-worker' };
: Worker reflections were shot down by the Chrome team when I brought this up. I don't know the exact reasons, but I believe efforts are best focused on blank workers for now here per the hoped worker.addModule
APIs. In turn import reflection will be designed to be compatible with this for transferring modules to workers so I think between those we should hopefully capture similar use cases.import x from 'y' with { assert: { integrity: '...' }};
, import x from 'y' with { assert: { referrerPolicy: '...' }};
: These assertions could certainly be specified, it would be very interesting to see further discussion form on new assertions, that's something that hasn't happened enough yet.import svg from './foo.svg' with { type: 'dom' };
: Can this be implemented like JSON and CSS modules as a content-type-based module type?import svg from './foo.svg' with { type: 'image' };
: I'm not sure I understand what the expectation is for this one in the browser?import stylesheet from './foo.css' with { media: 'print' };
: The media query discussion above was very interesting. This proposal suffers from similar concerns as was discussed - how to deal with nullability and changes? import stylesheet from './foo.css' with { layer: 'utilities' };
: What is the expectation here for utilities
?import audio from './file.mp3' with { loop: true };
: I'm not sure I understand what this one would do.import img from './foo.png' with { width: 500 };
, import img from './foo.png' with { convertTo: 'jpeg' };
: I think you're going to have a hard time getting this specified anywhere? It seems these are the only ones arguing for a custom space for bundlers to have their own semi-standard meta import system? To me it would make a lot of sense if module references didn't have evaluator attributes but asset references did:
import module x from "./foo.js";
// x is a module-block-like
await import(x);
import asset y from "./bar.png" with { type: "image" };
hostUseImage(y);
There is a need to separate these two because tc39 could have a lot to say about x
and would have very little to say about y
.
i.e. I think module references are a special case where most of what they do can be understood at the vanilla level, they should preload deps as static imports would and be dynamically importable. Whereas arbitrary host-managed evaluator attributes and all of their use cases would be much better suited to asset references, which already export arbitrary host-defined values.
In other words, as soon as a module reference includes host-specified evaluator attributes to make it yield a host-specified value, it becomes an asset instead.
Rather than lamenting module
vs asset
not being selected by an extensible scheme, we should accept module
as a necessary carve-out for importable module references, and let asset
be the extensible category.
Ok, I'll try to go through them with some more explanation and also try to answer @guybedford's questions along the way.
import mod from './foo.wasm' with { type: 'module' };
This was meant to be the exact same thing as the current proposed syntax import module mod from './foo.wasm'
. I used type
as a key here rather than reflect
because I think it's more understandable to the average dev, but it's the same. Confusion with import assertions may be an issue though. Again, the name isn't really important for the example.
import img from './foo.png' with { type: 'url' };
I was thinking this would supersede the asset references proposal. Perhaps type: 'asset'
would be better. Idea would be to return a constructed URL
object. new URL
or import.meta.resolve
are also fine syntaxes, but more dynamic. Having a static syntax could also be useful potentially. Many bundlers support importing assets as resolved urls like this today because it's pretty ergonomic.
import img from './foo.png' with { type: 'arraybuffer' };
(and similar for dataURL, or other formats)
This would return the data for the image as an ArrayBuffer
. Yes, you can do this with fetch, but doing so with an import statement has some benefits. It's statically analyzable, which is good for tools. It's also good for browsers, which could start downloading these resources much earlier (i.e. in a prescanning phase).
import worker from './worker.js' with { type: 'worker' };
(and service worker)
This would return a constructed Worker
object. Again, the benefits are that it's static, and that the worker can be downloaded in parallel with the rest of the module graph, without waiting for JS code execution to run the Worker
constructor. Sure, you could use link preloads, but this is nice and ergonomic. Also relative urls in the Worker
constructor are resolved relative to the page, not the current module, so usually you have to use import.meta.url
to resolve the url manually as well.
import svg from './foo.svg' with { type: 'dom' };
This would return a parsed SVGElement
DOM instance for an SVG file, which could be inserted into the document. Same benefits as above. Could be implemented with import assertions like CSS/WASM modules too, but if there are multiple possible reflections for an SVG (e.g. an Image
object, as described below), then there might also need to be a way to switch between them.
import svg from './foo.svg' with { type: 'image' };
I was thinking this would return an Image
object. Same benefits: early loading, static dependencies, relative resolution.
import {x} from "y" with { lazyInit: true };
See https://github.com/tc39/proposal-defer-import-eval
import stylesheet from './foo.css' with { layer: 'utilities' };
Idea here would be to be able to apply attributes to a reflected object, in this case a CSSStyleSheet
. In CSS, the @import
rule supports applying a cascade layer to an imported stylesheet, so why not JS as well? Could be useful when importing CSS stylesheets to have control over the cascade without making a copy of a CSS file wrapped in a @layer
rule.
import stylesheet from './foo.css' with { media: 'print' };
Similar to above, could apply a media query just like you can with @import
in CSS. The discussion about what to do when the media query doesn't match on initial load but does later is a good one. I guess I would expect it to behave the same way @import
and <link>
with a media query do: it would still be downloaded (at low priority), just not applied. https://medium.com/@tomayac/why-browsers-download-stylesheets-with-non-matching-media-queries-eb61b91b85a2
import audio from './file.mp3' with { loop: true };
Same idea: applying an attribute to an imported Audio
object. 🤷
import img from './foo.png' with { width: 500 };
, import img from './foo.png' with { convertTo: 'jpeg' };
Goal here was to show that with an extensible syntax, tools could support custom transforms using the same syntax. I would not expect these to be standardized. Tools could strip these before shipping compiled JS to clients.
To be clear, my goal with these examples is to show that there are a wide range of potential use cases for import reflections and attributes. Some of these could eventually be standardized, either by TC39 or by other standards bodies (e.g. as a part of the CSS module script spec). Some of them could be implemented in tools and stripped before reaching runtimes. Making the syntax extensible now will avoid needing to specify new keywords or syntax later for each possible reflection or attribute. This is important not just for tools, but also for runtimes.
What would importing an image do outside of a browser environment?
The language also needs to ensure it stays universal for environments beyond browsers and node, like IoT devices and other chipset environments, and the mental and implementation cost of having syntax (as opposed to API) that works in some environments but not others can be quite high.
I think the host runtime should "register" the attributes that it supports with the JS engine, and unknown ones should be rejected. This would allow browsers to have different supported attributes from other environments, and would match the behavior of import assertions.
@ljharb The language also needs to ensure it stays universal for environments beyond browsers and node, like IoT devices and other chipset environments, and the mental and implementation cost of having syntax (as opposed to API) that works in some environments but not others can be quite high.
ES6 module resolving and loading is already environment-specific. import.meta
is already environment-specific. There's nothing wrong with having a feature which varies between different environments. And having extensible syntax makes things better for different environments, because each environment can define its own custom properties.
If extensible syntax doesn't exist, then every environment must either invent its own syntax for environment-specific things, or it must create a new TC39 proposal, which isn't always desirable or possible. And if the feature is environment-specific, then it's unlikely to be accepted as a TC39 proposal. Extensible syntax fixes all of those problems.
Rollup just landed https://github.com/rollup/rollup/pull/4646, which adds support for arbitrary assertions and custom processing of those assertions by plugins.
The bundler ecosystem continues to land on the same solution, and I think it's because it's extensible.
With import assertions having transitioned to import attributes (slides) and import reflections transitioning to import phases (decoupling from attributes in the process), I think this issue is addressed.
The use cases brought up are relevant to and satisfied by import attributes now. You can see them used with their extensible syntax in conjunction with phases in one of the slides: https://docs.google.com/presentation/d/1Abdr54Iflz_4sah2_yX2qS3K09qDJGV84qIZ6pHAqIk/edit#slide=id.g216c73c7b74_0_35
(Or at least unless anyone thinks the phase should use an extensible syntax?)
at least allow booleans plz. only allowing string properties is annoying when I try to use them.
This has been resolved, as this proposal is specifically dealing with only import phases.
As import attributes is now stage 3, please direct any feedback regarding the import attributes options bag to the TC39 Discourse.
Would you be open to using an extensible key/value format, to future-proof reflection in case it's needed for other use cases, even though none are implemented now?