4

I have a plain JS script, parser.js (generated from a tool) that depends on variables defined in an ES6 Module, lexer.js. In my ES6 module I have already exported the variables to the window object so they are accessible from parser.js. However, I need to somehow run that ES6 module before running the script. And there doesn't seem to be any way to do so.

Attempt 1: Try to load ES6 Module synchronously before including my script

I tried some thing like this in my HTML.

    <script src="lexer.js" type="module"></script>
    <script src="parser.js"></script>

But it doesn't seem to run in order. lexer.js runs after parser.js

Attempt 2: Try to load script synchronously inside an ES6 Module

I tried creating a wrapper ES6 module around my parser script like so

// use import to run the module and load variables into the window
import { lexer } from './lexer.js';

// load parser script synchronously
var req = new XMLHttpRequest();
req.open('GET', 'parser.js', false);
req.send(null);
eval(req.responseText);

However, it seems like synchronous XMLHttpRequests are deprecated and don't work anymore (edit: actually they do, see my answer below), and I can't find any other way to synchronously load a script. Overall, I would say the incompatibilities between the ES6 module system and the old javascript include system, to be beyond frustrating.

P.S. For reference, the code generation tool I am using is the Nearley grammar compiler, which allows me to reference my lexer from the grammar, and generates a plain JS parser.

EDIT: @yong-quan suggested a neat solution, to simply put defer in the script include tag, eg

    <script src="lexer.js" type="module"></script>
    <script src="parser.js" defer></script>

It seems like this simply defers the execution of parser.js to the end. However, I failed to mention that I actually have an ES6 module called interpreter.js that needs to be called after parser.js. Sorry for not mentioning that sooner, I assumed that whatever solution worked for my first issue would also solve my second issue. I fixed the title to clarify that I need ES6 modules to run before and after my plain JS script. Essentially, I need to integrate this plain JS script into my module dependency graph.

EDIT2: I was wrong, the defer solution works. See @Aviad or my own answer below

woojoo666
  • 7,801
  • 7
  • 45
  • 57
  • Have you tried ``? – yqlim May 10 '19 at 00:56
  • wait actually, I'm not sure if that will suit my needs because I actually have a ES6 module that has to run _after_ `parser.js`, called `interpreter.js`. I didn't mention it in the question because I assumed that any solution to the first problem would solve this second problem, but I guess that isn't the case. I'll include that final piece in my question. Thanks for the suggestion though – woojoo666 May 10 '19 at 01:25
  • 2
    If parser.js appears in html before both other scripts, then you can defer parser and interpreter (it will be executed in that order) and lexer without defer will be first. – bigless May 10 '19 at 02:16
  • you're right, @aviad suggested the same thing. In fact I just added `defer` to all scripts, including `lexer.js`, and it still executed in the correct order. Weird, but works for me :) – woojoo666 May 10 '19 at 03:49

2 Answers2

2

I think that good practice here will be to create some kind of download manager for this (by using webpack chunk loading/dynamic imports for example)

Another option is to use defer attributes. Note that the defer indicates that the scripts run in the order they were encountered, thus you can assume parser.js is loaded when you call interpreter.js if the order is correct.

Links:

Aviad
  • 3,424
  • 3
  • 14
  • 21
  • wow, you are right, I added `defer` to all my scripts, and it started running them in the correct order. Very weird, since without `defer` it is still supposed to run them in the order they are encountered, but w/e this solves my problem, thanks! (and I am definitely considering moving to webpack in the future) – woojoo666 May 10 '19 at 03:31
  • w/o `defer` you'll have a race condition – Aviad May 10 '19 at 23:46
  • why would it be any different, putting `defer` in front of all scripts vs none of them? – woojoo666 May 11 '19 at 01:33
  • @woojoo666 assume your page fetches 3 scripts: A, B, and C (they also appear in that order). Now, let us assume that for some reason (size, bandwidth, etc) B will be fetched "faster". Now, one can claim that the order still persists - thus A will be fetched & executed before B. BUT! since HTTP 1.1's persistent connections, the browser can fire multiple requests to fetch resources, which can lead to scripts being executed not in "encounter order" – Aviad May 11 '19 at 15:30
  • wouldn't that be against the spec though? At least for non-ES6 scripts, so many pages would break if scripts weren't executed in order. To me it seems like the problems is that ES6 modules don't follow the same order (unless you add `defer`), which is what makes it so hard to mix old scripts into the module system – woojoo666 May 12 '19 at 16:54
0

So I solved the issue using @Aviad's solution (posted above), and added defer to all my scripts, eg like so:

    <script src="lexer.js" type="module" defer></script>
    <script src="parser.js" defer></script>
    <script src="interpreter.js" type="module" defer></script>

and it started loading them in the correct order. Neat! Though I think it's important to note that, I haven't found an actual specification for the load order of ES6 modules, so it seems like there is no guarantee for the load order (aside from the case where one module depends on another, in which it would load dependencies first). So while the defer trick works for now, I think it's possible that it may break in the future.

I'd also like to mention that, it turns out my XMLHttpRequest synchronous script loading was actually working, I was just running into some scoping issues. I had to call eval from the scope of the window, because normally parser.js is loaded as a <script> tag in the HTML, so it expects the scope to be window, so I needed eval() to emulate that, like so:

// load parser script synchronously
var req = new XMLHttpRequest();
req.open('GET', 'parser.js', false);
req.send(null);
eval.call(window, req.responseText);

So I guess I have a fallback if I need it. I was also considering manually converting parser.js to be an ES6 module, but this saves me the hassle of converting it every time I re-generate it. Thanks @Aviad for your solution!

woojoo666
  • 7,801
  • 7
  • 45
  • 57