A Bug's Life

I've spent a little bit of time here and there espousing the benefits of using JavaScript Components in Magento 2, and removing inline JavaScript, so that we can better mitigate XSS attacks, and so that we can better optimise our websites by removing any stateful HTML from the initial page load.

I say this as emphatically for the admin area as I do for the front end. But Magento has other ideas. I've been building a few admin interfaces recently, and I happened upon this catastrophic collision of old and broken code. It made me think about how sometimes good people write bad code, and we shouldn't judge them too harshly.

Are we sitting comfortably?

Good, then I'll begin.

I was creating a custom interface to allow orders to be placed in the back end of Magento to have a time slot selected. Similar to how you select a delivery hour when you buy groceries online. It was using Knockout, and I was feeling jolly proud of myself, but it only loaded on the second load. When you first visited the order creation screen, it wouldn't show up.

My colleague suggested that rather than the sales_order_create_index layout handle, I might be better off with the sales_order_create_load_block_data handle. I tried that, and was greeted with an altogether different problem.

On the first load, my delivery selection app would show up, but died with some syntax error. On the second load, it simply didn't show up. Perhaps the handles aren't equivalent, and perhaps I need both? After including both, I was back to a better situation: broken (but not missing entirely) on the first load, and correct on subsequent loads.

They say that one can measure progress in software development by marking movements from one error message to another, and sure enough, I was in a better situation.

The error was interesting, though. I had my standard JSON declaration of a JS component, something like this:

<script type="text/x-magento-init"> { "*": { "Magento_Ui/js/core/app": { // args... } } } </script>
JS Component allegedly causing a syntax error

I knew that JSON could be funny about trailing commas and double quotes, but this worked on subsequent loads and should have been the same. Just in case, I copied the JSON inside the element and ran pbpaste | jq . through my terminal. All was well.

So what if it was trying to parse it as JavaScript? By pure coincidence, I learned just over the weekend that the below JSON expression is not valid JavaScript:

{"key":"value"}
A simple JSON object

A parser interprets the first opening brace as a new block, and then crumbles around the colon. Enclosing the above expression in parentheses (( and )) will fix this problem.

Having a deeper look at the syntax error revealed that it was coming from an eval(...) call inside legacy-build.min.js. In Magento 2, this is the file that contains the Prototype library. (It's actually mapped. If you require "prototype" in your JavaScript, it loads this file.)

So Prototype was trying to run my JSON as JavaScript, and that was breaking it. But why? Well.

It turns out that the order creation screen is actually a rather complicated multi-stage page that reloads itself and renders different things at different times depending on the state. So when we first visit that page by clicking the "Create Order" button on the order grid, we see a Customer selection grid. After selecting a Customer, we are invited to select a Website. After doing that, we go to the Order creation screen proper.

But it didn't reload the page! It used a little feature of Prototype called Ajax.Updater. It performs an AJAX request and then updates the innerHTML of some element with the response. As an added bonus, it also executes any inline scripts for you, because that won't just happen on its own. From the documentation:

If your HTML comes with inline scripts, they will be stripped by default. You'll have to pass true as the evalScripts option in order to see your scripts being executed.

The Magento core depends on this behaviour, and so it is enabled. To be fair, if it was disabled I'd be in an even worse situation. The problem is that the browser is smart enough to not execute inline script tags when it doesn't recognise their type attribute. Prototype, coming from before the dawn of time, apparently has no such understanding.

Nevertheless, now that I understood the problem, the solution was relatively straightforward: Just sell my soul to the devil and write it the way the rest of the admin area is written.

<script> require([ 'Magento_Ui/js/core/app' ], function (app) { return app({ // args... }); }); </script>
Never do this. Unless you have to

Ta-dah. A Bug's Life.