whatwg / html

HTML Standard
https://html.spec.whatwg.org/multipage/
Other
7.98k stars 2.6k forks source link

Suggestion: You should be able to load module-scripts in the `file://` protocol. #8121

Open CalinZBaenen opened 2 years ago

CalinZBaenen commented 2 years ago

You should be able to load module-scripts in HTML via the file:// protocol without a CORS error. - Here's why.

So, one gripe I have with JavaScript is that "everything cool" is locked behind CORS. I don't seem to be the only one with this sentiment either, or at least we share the sentiment that we shouldn't need CORS-validation, as indicated by this issue, #1888, "is mandating CORS for modules the right tradeoff?".
Maybe it makes sense to lock cookies behind CORS, maybe it makes sense to lock reading files, even local ones, behind CORS. - But preventing importing code from another JavaScript file in the same, local directory, or any of its subdirectories, makes NO sense, and this Issue will explain why.

What are JS modules?

Sure. We should all know what JavaScript modules are, but there are some lurkers here, and I think regardless it would be nice to actually recap what exactly this feature is.

TL;DR A JavaScript E6 module is an easy way to package code together in an organized and orderly fashion, unlike what you'd get if you loaded a bunch of <script src="./filename.js"></script>s.

A JavaScript module lets you share code between one or more files by using import/export syntax, which isn't too different from most other languages.
So, how do we get started? - Well, first, create a file called test.mjs, .mjs is the extension for JavaScript Modules (Modular JavaScript?) and enter the following:

export class Person {
    constructor(name, age) {
        this.name = name ?? "";
        this.age  = age  ?? 0;
    }

    name;
    age;
}

Then, create a file called main.js and enter the following:

import {Person} from "./test.mjs";

let calin = new Person("Calin", 15);

alert(`${calin.name} is ${calin.age} years old.`);  // Should alert "Calin is 15 years old.".

Now, all you have to do to use these files is import them into an HTML file, and that's easy enough:

<script type="module" src="./main.js"></script>

Now all you have to do is double click and viol-... Oh.. Nothing?
Oh, it's a CORS error.
This is what it looks like for me, someone using Chromium on x86_64 Arch Linux:

Access to script at 'file:///home/calin/Downloads/test/main.js' from origin 'null' has been blocked
by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome,
chrome-extension, chrome-untrusted, https.

To get this to work you need to run a server using something like node.
Once you do this you can finally alert my name.

[For Firefox/Librewolf, see this comment.]

What is an example of something someone would want to do?

So, I, like many people, sometimes like to get distracted and make a lot of random side projects they think would be fun to take on.
For me I wanted to make something with the HTML5 <canvas>ses dubbed "Drawr", it'd make and manage canvases for you while you called builder-pattern like methods.
For example you'd have this "GfxArea" type:

export class GfxArea {
    /* ... */
    #antialias = false;
    #pheight;  // Physical height. (Height in HTML/CSS.)
    #pwidth;   // Physical width.  (Width in HTML/CSS.)
    /* ... */
}

Now, lets say I wanted to use this type in another file.
Maybe I'm constructing it. Maybe I'm using one of its static properties for something.
Either way, what I do with it should be up to me as the programmer.

What solutions are there currently?

There's two solutions. And only one is feasible if you want to make an HTML page that you can redistribute to anyone.

  1. We (and our clients) run a server every time we want to use the app, or we host it on some kind of service.
  2. We have to spam <script> tags from <script src="./least_dependent.js"></script> to <script src="./most_dependent.js"> </script> rinse and repeat for however many libraries or extra program files we have, then we can finally do <script src="main.js"></script>.

So, option one.
That ain't so bad, is it? By most people's standards, no, it isn't.
By my standards, as someone who wants to do something simple, and cannot afford special services for hosting stuff, yes, it really is. - Not the end of the world bad, but it's up there.

Why's it bad? JavaScript is actively preventing HTML applications (intentionally) made for usage in the file:// from having neater code.
Apparently the reason we're even allowed to use <script> at all, according to @domenic, is because it was shoehorned in for compatibility reasons.

Many years ago, perhaps with the introduction of XHR or web fonts (I can't recall precisely), we drew a line in the sand, and said no new web platform features would break the same origin policy. The existing features need to be grandfathered in and subject to carefully-honed and oft-exploited exceptions, for the sake of not breaking the web, but we certainly can't add any more holes to our security policy.

To me this is a poor excuse and sounds like stubbornness.
Sometimes exceptions should be made when appropriate, and I believe now is that time because it would greatly reduce the amount of <script> tags needed in web-apps that run on the file:// protocol, such as games being made by people who are new to the HTML/JavaScript ecosystem and would greatly increase the productivity of people who use JavaScript in their HTML.


Lets see what option two can do far us...
You can probably already see the problem with this strategy, but it at least makes it possible to make applications that work on the file:// such as games made by newbies to the HTML and JavaScript ecosystem.
... But it's incredibly inefficient.

There are a few main issues I want to bring to attention:

Maybe these aren't great justifications, but there equally isn't great justification for gatekeeping it.

What changes would need to be made? (Especially in regards to Browsers.)

No changes would really need to be made other than import scripts, at least in the file:// protocol, shouldn't be fetched through CORS, and if they are this specific scenario should be exempt so long as the imported file doesn't "leak outside" of the top-most folder. - The top-most folder would be decided by what HTML file is running.
For example:

my_app/
    index.html
    test.js
    src/
        main.js  # This file is fine to import "./some_file.js", "../test.js", and "./sub_library/another_file.js", but it could not do "../../file_name.js".
        some_file.js
        sub_library/
            another_file.js

Why we shouldn't allow module-scripts in the file:// protocol.

I can't think of a single legitimate reason that this shouldn't happen.
If you'd like, block quote this and the previous sentence and leave your reply.

Even, as cited before, @domenic, confirms that there really isn't an incentive to keep this walled off other than "New features must use CORS, no exception.".

The web's fundamental security model is the same origin policy. We have several legacy exceptions to that rule from before that security model was in place, with script tags being one of the most egregious and most dangerous.

It's not about a specific attack scenario, apart from the ones already enabled by classic scripts; it's about making sure that new features added to the platform don't also suffer from the past's bad security hygiene.

However, in direct response to that last quote, there's no way module scripts could face "bad security hygiene" because they're already implemented and have already been tested, porting the functionality over the the local-scope of the file:// protocol shouldn't, in theory, be too difficult of a task, especially considering a number of environments support these ES6 modules.

Final Notes

I believe a more flexible kind of scripting, one with packages, ES6 modules, should come to the file:// protocol to the file protocol because its absence is confusing and has been the cause of a few StackOverflow posts, including: ES6 modules in local files - The server responded with a non-JavaScript MIME type, Javascript module not working in browser?, and Browser accepts "classic" js script-tag, but not ES6 modules — MIME error w/ Node http server.
Module scripts also, when fully evaluated, have the same effect as loading a bunch of individual scripts, therefore it poses no threat to already-existing technologies.


CC: @domenic @annevk @jonco3

CalinZBaenen commented 2 years ago

I reiterate over the reasons in a concise fashion on DEV in You should be able to use module scripts in the file:// protocol!.
It will basically act as a recap to my post:

  1. In the end module scripts have the same effect as loading in multiple dependencies through <script src=""></script>.
  2. For every HTML file that relies on a dependency you must write out all the <script> tags needed to point to every required file, and the scripts have to all be in dependency-resolution order.
  3. It will be hard to debug if you have a lot of dependencies and miss one file or have a typo.
  4. You can't have any of your scripts, dependencies or your main JavaScript files, be async or you risk them loading in the wrong order.
  5. Testing JavaScript code, especially packages, is more difficult without easy access to modules.
  6. The ES6 import/export syntax looks way nicer than a bunch of <script src=""></script>s being spammed.
  7. It can confuse people who don't know about CORS, especially for someone who doesn't understand why this would be an error in the first place.
  8. It already has been the source of confusion as at least three StackOverflow articles have been created on the premise of "Why doesn't my module work in the browser?".
  9. Almost no changes need to be made for it to be utilized by browsers (or by web developers).
  10. The only reason it isn't allowed is because "New features shouldn't be allowed to get around CORS. No exceptions.".
annevk commented 2 years ago

Why are file: URLs important to you? If anything I think we should invest in making them less necessary to deploy applications.

CalinZBaenen commented 2 years ago

@annevk

Why are file: URLs important to you?

Because I, for example, want to redistribute a project, maybe a small app, maybe a game, just something I did for fun, to other people so they can then in enjoy it.
I don't want to, nor should I have to, set-up a web-server just to host it.
And is it reasonable that I expect my end-client, my friends who don't know much about tech/programming, or the random people that download it from me, to set up a server so they can host it? - Personally I don't think it is, but maybe we're just not seeing eye to eye.

CalinZBaenen commented 2 years ago

But maybe I suffer from "little kid" syndrome and complaining for no reason because I'm just a lazy fifteen year-old that uses Arch.

pshaughn commented 2 years ago

I would probably use this. I like being able to make the online and offline versions of a small Javascript project the exact same files, instead of having to package them differently and keep track of builds. (This is especially convenient with the way publishing to itch.io works.) I've had some success using the tool "Browserify" to hammer module-based code into the classic-script square hole, but it's not seamless.

CalinZBaenen commented 2 years ago

@pshaughn

I've had some success using the tool "Browserify" to hammer module-based code into the classic-script square hole, but it's not seamless.

Hammer it?
So it's forcefully shoved into this position it (probably) shouldn't be in?
I mean, that explains why it wouldn't feel seamless.

CalinZBaenen commented 2 years ago

Upon further examination I have deduced that this would also be good for web-design or JavaScript classes.
This could be utilized to teach students about modular program design while staying relevant to the web or graphics design course.

CalinZBaenen commented 2 years ago

Also, as @dveditz claims, people at Mozilla haven't reached a proper consensus on whether CORS for such a feature is necessary:

The Mozilla folks are reviewing this design and are not uniformly convinced that CORS is necessary for a default no-credentials, content-type restricted (which we already wanted) request. Obviously we have not reached internal consensus but didn't want silence to be interpreted as agreement.

With that said, that means this idea isn't fully off the table and I'd like to at least appreciate that. :)

CalinZBaenen commented 2 years ago

@annevk

If anything I think we should invest in making them less necessary to deploy applications.

By the way, what do you mean by this?
How are they necessary currently?
I just see them as a tool, and a convenient way to have a document on your computer.

nerudaj commented 2 years ago

@annevk

Why are file: URLs important to you?

Because I, for example, want to redistribute a project, maybe a small app, maybe a game, just something I did for fun, to other people so they can then in enjoy it. I don't want to, nor should I have to, set-up a web-server just to host it. And is it reasonable that I expect my end-client, my friends who don't know much about tech/programming, or the random people that download it from me, to set up a server so they can host it? - Personally I don't think it is, but maybe we're just not seeing eye to eye.

This so much. I am not a web dev, but I am currently working on a small game using web tech. It is great it works on PC, Xbox, Android and others outside of the box, but the requirement for having a web server is super annoying. More so when Gamepad API needs to be in secure context, which applies file:// protocol (yay) and having a secure context for your web server is a rabbit hole nobody wants to delve in when they do small side project for fun.

CalinZBaenen commented 2 years ago

@nerudaj, while I like the idea some things should be reworked to allow the file:// protocol, I feel like that's quite a big request to make.
It also isn't really relevant as this Issue is specifically for ES6 module support.

nerudaj commented 2 years ago

@nerudaj, while I like the idea some things should be reworked to allow the file:// protocol, I feel like that's quite a big request to make. It also isn't really relevant as this Issue is specifically for ES6 module support.

I am not sure if we understand each other. I am not proposing any new feaures, I just expressed support for your issue as the exactly the same problems as you have are troubling me as well.

I might have not been clear in regards to secure context - some cool features are locked behind secure context (which applies to file protocol as well) and some are locked behind CORS (like ES6 modules). But since the CORS lock does not overlap with secure context (and I am not experienced enough with web to understand why), there is this weird space where you can have A or B but not both at once, unless you deploy a web server and pay for certificate for HTTPS.

But I would have been perfectly content if the stuff you are proposing was implemented as it would cause the overlap I currently lack.

Kaiido commented 2 years ago

Aren't all these use cases better handled by the many online services available, like jsfiddle, codepen, stackblitz, glitch, etc. ? Some of these even offer to link to your version control service so you can "work together" in good conditions, they even come with plenty of various prebuilt configs et al.

As for sharing, it's just a link, while with js files directly you'd need to manually bundle all these files, at least in a zip file, each of your users would then have to unbundle it, if they do modifications to it that soon becomes a nightmare to know who has which version.

And it's accessible to any device.

CalinZBaenen commented 2 years ago

@Kaiido but using tools like JSFiddle you see the code the user may be confused or scared off by that. I don't want that.
Also that brings back the problem of only being available online. (Possibly?)

Kaiido commented 2 years ago

Some of these services like glitch.com do offer single link to the preview directly, you don't need to have the code next to it, it's a real website.
You can also install a ServiceWorker to make your assets available offline if that's a must.

Ps: And people should be more afraid of downloading something on their computer and then running it than running it from the web directly.

CalinZBaenen commented 2 years ago

You can also install a ServiceWorker to make your assets available offline if that's a must.

Well I'd prefer to have the whole game offline.

CalinZBaenen commented 2 years ago

Ps: And people should be more afraid of downloading something on their computer and then running it than running it from the web directly.

True but browsers are restrictive for that reason. I can get behind that. I think it's still fun to use HTML and JS this way. - Consider me making a little 2D Minecraft/Terraria like clone in HTML/JS for fun because those are easy to make simple projects in.
Or take a look at E3D, something I couldn't of made when I was as young as I was if JavaScript and HTML wasn't as accessible as it is now.


Regardless though, this isn't about removing restrictions, this is about breaking down an necessary boundary.
The behavior is of modules is something we can already emulate if we run the scripts in the exact order they need to be loaded in.

nerudaj commented 2 years ago

Ps: And people should be more afraid of downloading something on their computer and then running it than running it from the web directly.

What is worse? People downloading something from github where they can check the code to see if it malicious or downloading homebrew APK they can't check? You cannot expect people will stop doing unsecure things when the alternative means they have money/skill to deploy web app or go through app verification process to get it on the store.

When people are learning, they need to learn fast, not to be bogged down by production setup (they need to learn that at some point, but a later one). And they usually have friends who are willing to be testers for their stuff. And it is really nice to write a multiplatform app that I can deploy on my device and use it without internet (which is exactly what I am using Javascript for).

Yes, people should be cautious what they are downloading off the net, but I don't see that as a counter argument for the issue in question.

Aren't all these use cases better handled by the many online services available, like jsfiddle, codepen, stackblitz, glitch, etc. ? Some of these even offer to link to your version control service so you can "work together" in good conditions, they even come with plenty of various prebuilt configs et al.

Well.. internet is still not available for everyone, everywhere, so working exlusively in a cloud environment is not ideal. And I doubt they're as good IDEs as VS Code is.


The original discussion this issue spun from was about "will CORS requirement negatively impact adoption of modules". For me, the answer is yes. As @CalinZBaenen summarized it, we can develop using legacy features or as @pshaughn mentioned, we can use hammers of questionable quality to convert code to classic script tags. But is that really necessary?

CalinZBaenen commented 2 years ago

@nerudaj

The original discussion this issue spun from was about "will CORS requirement negatively impact adoption of modules".

Actually, this isn't how I originally approaches the issue.
I approached it the same way I did here, by ranting about CORS.
However I think this would be an interesting entrypoint for the issue, and maybe I should assess this that way in a later comment.

CalinZBaenen commented 2 years ago

Actually, this isn't how I originally approaches the issue.

I think part of the reason I thought this would be a hit of a feature is because languages like Rust, Java, C++, Python, etc... all support some kind of componentization regardless of the program's "context" (the environment it's running in*).

CalinZBaenen commented 2 years ago

One final note about modules is that they'd give us, and not much, but at least a little bit of a namespacing system for libraries, which is something that'd be suuuper helpful when debugging in local HTML and JS files.

CalinZBaenen commented 2 years ago

So, I've thought about my argument from the whole "will CORS requirement negatively impact adoption of modules" angle.
And, while it's against my own beliefs or what I want available in the file: URI scheme, no.

Even though it's a weak point against my argument not to have module-scripts, because they inherently need to be locked away for actual security purposes, having a CORS restriction didn't impede the adoption of XMLHttpRequest and fetch in peoples' code.

we can use hammers of questionable quality to convert code to classic script tags.

IMO the worst part is that "normal", inline <script> tags can't be async or deferred but modules can be.
Now, people reading this may be like "bUt yoU cAn pUt async and defer on NoRMal <script> tAgs iF thEy HAve A sOurCe.", and yes, I'm aware dear reader.
The problem is, when you're importing a library file by file and try to use async, that library will come crashing down because there's only a small chance that all scripts will load in the right order.


CC: @nerudaj, @pshaughn

CalinZBaenen commented 2 years ago

Oh hey. Look what I found.
Yet another StackOverflow question on why their script got denied.
. . . Could you possibly guess why?

nerudaj commented 2 years ago

So, I've thought about my argument from the whole "will CORS requirement negatively impact adoption of modules" angle. And, while it's against my own beliefs or what I want available in the file: URI scheme, no.

Even though it's a weak point against my argument not to have module-scripts, because they inherently need to be locked away for actual security purposes, having a CORS restriction didn't impede the adoption of XMLHttpRequest and fetch in peoples' code.

I wasn't suggesting it impedes the adoption for everybody. I meant that for me, this is a serious argument why to not adopt modules. Somebody might feel the same, others don't. That was just my stance.

domenic commented 2 years ago

Hey folks. A gentle reminder that every time you comment here, it emails the 648 watchers of this repository. If there are no new technical arguments being made, I suggest taking this discussion elsewhere; the proposal is already pretty clear.

CalinZBaenen commented 2 years ago

@domenic Where do you suggest I bring the tangential conversation?

CalinZBaenen commented 1 year ago

So, I'm writing some JavaScript for a project I want to have on my webpage locally, in my filesystem, and my JS file is huge (178 lines so far).
The main chunk of it is a huge class with methods, but I feel like I could make it neater if I had access to modules so I could reliably split my code up into functions (without having to worry about <script> loading order).

Here's an obfuscated version of my class with comments removed, names shortened and obfuscated, and minimal newlines;
just imagine how it would look with doc-comments, comments, whitespace, proper but long names, etc...

class PgmIntf {
    static sg(v=0) { PgmIntf.color = (PgmIntf.color & 16711935)+((g & 255) << 8); }
    static sb(v=0) { PgmIntf.color = (PgmIntf.color & 16776960)+(b & 255); }
    static sr(v=0) { PgmIntf.color = (PgmIntf.color & 65535)+((r & 255) << 16); }

    static t15(v=0, m=undefined) {
        if(typeof v !== "number" || Number.isNaN(v)) v = 0;
        if(v === 0 || v === 16777215) return v;

        const v1 = v & (255 << 16);
        const v2 = v & (255 << 8);
        const v3 = v & 255;

        if(m?.constructor != Map) m = P15;
        {
            const s1 = map.get("1")   ?? [0, 255];
            const s2 = map.get("2") ?? [0, 255];
            const s3 = map.get("3")  ?? [0, 255];

            const ld1 = Infinity;
            const di1 = NaN;
            const ld2 = Infinity;
            const di2 = NaN;
            const ld3 = Infinity;
            const di3 = NaN;

            for(let sn = 0; sn < s1.length; sn++) {
                const diff = Math.abs( s1[sn]-v1 );
                if(diff < ld1 || lw1 === Infinity) {
                    ld1 = diff;
                    di1 = sn;
                }
            }
            for(let sn = 0; sn < s2.length; sn++) {
                const diff = Math.abs( s2[sn]-v2 );
                if(diff < ld2 || ld2 === Infinity) {
                    ld2 = diff;
                    di2 = sn;
                }
            }
            for(let sn = 0; sn < s3; sn++) {
                const diff = Math.abs( s3[sn]-v3 );
                if(diff < ld3 || ld3 === Infinity) {
                    ld3 = diff;
                    di3 = sn;
                }
            }
        }
        /* <Incomplete...> */
        return COMPUTED_VALUE;
    }

    static #update() {
        const e = document.getElementById("sitespecific-element");
        try {
            const code = ColorpickerProgram.#hex;
            e.style.property = code;
            e.title = code;
        } catch(e) {
            console.info(`${e.message}: ${e.name}`);
        }
    }

    /* <Omitted methods...> */

    static set map(v=null) {
        if(v == undefined || v == null) v = undefined;
        if(v?.constructor === Map) this.#cm = v;
    }
    /* Getter for `map`. */

    static #cm;
    static #cl;
    static #hx;
}
mjkjr commented 1 year ago

I'm frustrated by this issue too, so I wanted to add my two cents:

I'm recently trying to follow a vanilla javascript game dev course on youtube provided by freeCodeCamp. Everything has been in a single, growing JS file. I landed on this issue thread after trying to split out some classes into a separate JS file for organizational purposes and discovering that, hey! F me, that's not possible without spinning up a server!

This is a personal project that itself will never need to live on a live server. This is something that should not rely on an internet connection for me to work on. This is something intended to be basic for a beginner to slowly work up the learning curve.

I agree with @CalinZBaenen here that I should be able to work on a small side project without having to immediately shift my entire learning process to figuring out how to set up a local server just to organize my code.

I have made other small side projects before that were small local utilities and simple games which were also never intended to reside on an actual live server and similarly should have been able to benefit from the organization of files into ES modules.

I disagree with the suggestion to use JSFiddle/Codepen/etc because I should be able to run a set of local files without the requirement of an internet connection. Why should my small utility or beginner learning project depend on an internet connection?

I'm old and returning to programming after initially learning back in high school. Back then I could load up stuff locally and start learning easily. I'm trying to get my son into coding recently and this is issue just increases the learning curve on newcomers or forces them into less efficient organization of their code to avoid the issue.

Further, I fail to see where the security issue is here. A html document on the local filesystem is already capable of loading scripts file from the same filesystem, what difference does it make if those scripts loads additional files from the same scope? As others pointed out above, we could just as well include them via a bunch of Githubissues.

  • Githubissues is a development platform for aggregating issues.