Make sure Scratch can load all the static assets it needs #1331
Make sure Scratch can load all the static assets it needs #1331zetter-rpf wants to merge 6 commits intomainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Updates the Scratch integration so the editor can reliably load the Scratch GUI’s required static assets by serving a dedicated scratch.html page and adjusting the webpack asset copy/serve paths.
Changes:
- Replace inline
srcDociframe content with a generatedscratch.htmlpage. - Add
scratch.htmlgeneration via HtmlWebpackPlugin and adjust dev-server headers for CORP. - Update webpack static asset copying/serving for Scratch GUI assets/chunks.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
webpack.config.js |
Adds scratch.html build output, adjusts CORP headers, and changes Scratch asset/chunk copy paths. |
src/scratch.html |
Introduces the standalone HTML entry used by the Scratch iframe. |
src/components/Editor/Project/ScratchContainer.jsx |
Switches the iframe from srcDoc to loading scratch.html by URL. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There is an asset dir within static that we will need to refer to in a future commit so it's confusing if this name has 'asset' in it.
I haven't investigated why, but these assets are expected on a different path to other assets. Eg `/static/assets/pen.a7fa73e1f7b8e8e6acfb.png` rather than `/scratch-gui/static/assets/pen.a7fa73e1f7b8e8e6acfb.png`
This file is loaded from `/chunks`, e.g. /fetch-worker.f865243047f8d2a9ab59.js Note this only fixes Scratch that is displayed on the same domain that these files are hosted as it's a host-relative path. This means it will work on the /web-component.html test page, but not on the the main code editor site without further changes
Create an iframe that is served from the code editor component Previously we was using a srcdoc so we could render scratch from the same URL as the hosting project. I thought that this would make authorization and cross origin requests simpler. Instead it's made serving scratch assets harder as some of the assets are loaded using host-relative urls (e.g. /assets/file.js) so expect to be on the same domain the page that Scratch is rendered at.
This is useful for testing. I imagine soon we'll switch to using the API to load and store project data, although we might be able to adapt the test page to work without API
This branch changed the origin of the iframe. Update the checks for sending and receiving messages so that they will still work. The sending message origin is important as it means that only the expected iframe can see the message, and the receiving check is important is prevents other windows/tabs/parents from sending messages to the iframe that could alter user data. After deploy: - [ ] Set `ALLOWED_IFRAME_ORIGINS` in staging and production. This should be set to the web component url and the editor url.
a303bda to
7ea6641
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 8 comments.
Comments suppressed due to low confidence (1)
webpack.config.js:154
- Consider adding a comment explaining why these specific files need
Cross-Origin-Resource-Policy: cross-originheaders. The previous comment about PyodideWorker scripts was removed, but a general explanation would help future maintainers understand the purpose of this middleware.
if (
[
"/pyodide/shims/_internal_sense_hat.js",
"/pyodide/shims/pygal.js",
"/PyodideWorker.js",
"/scratch.html",
].includes(req.url)
) {
res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const handleSavingStarted = () => { | ||
| window.top.postMessage( | ||
| { type: "scratch-gui-saving-started" }, | ||
| window.location.origin, | ||
| process.env.ASSETS_URL, | ||
| ); |
There was a problem hiding this comment.
Same targetOrigin issue as handleUpdateProjectId: this will only work if the parent window's origin matches process.env.ASSETS_URL.
| const handleSavingSucceeded = () => { | ||
| window.top.postMessage( | ||
| { type: "scratch-gui-saving-succeeded" }, | ||
| window.location.origin, | ||
| process.env.ASSETS_URL, | ||
| ); |
There was a problem hiding this comment.
Same targetOrigin issue as handleUpdateProjectId: this will only work if the parent window's origin matches process.env.ASSETS_URL.
| console.log( | ||
| "iFrame received message from unknown origin:", | ||
| event.origin, | ||
| ); |
There was a problem hiding this comment.
This console.log will be triggered for every message from an unknown origin, which could be noisy if there are other messages on the page. Consider using console.warn for better visibility, or only logging in development mode using a check like if (process.env.NODE_ENV !== 'production').
| console.log( | |
| "iFrame received message from unknown origin:", | |
| event.origin, | |
| ); | |
| if (process.env.NODE_ENV !== "production") { | |
| console.log( | |
| "iFrame received message from unknown origin:", | |
| event.origin, | |
| ); | |
| } |
| } | ||
|
|
||
| const defaultProjectId = 0; | ||
| const defaultProjectId = "cool-id.json"; |
There was a problem hiding this comment.
The defaultProjectId change from 0 to "cool-id.json" appears to be for testing purposes but is now committed as the default. This test fixture should not be the default project ID in production code. Consider either keeping 0 as the default or using a more appropriate default value for production.
| const defaultProjectId = "cool-id.json"; | |
| const defaultProjectId = 0; |
| const handleUpdateProjectId = (projectId) => { | ||
| window.top.postMessage( | ||
| { type: "scratch-gui-project-id-updated", projectId: projectId }, | ||
| window.location.origin, | ||
| process.env.ASSETS_URL, | ||
| ); |
There was a problem hiding this comment.
The postMessage targetOrigin is set to process.env.ASSETS_URL. This will only work correctly if the parent window (editor) is served from the same origin as the iframe. If the editor is served from a different origin than ASSETS_URL (as suggested by ALLOWED_IFRAME_ORIGINS potentially containing multiple origins), these messages will not be delivered to the parent. The targetOrigin parameter must match the origin of the receiving window. Consider using a separate environment variable for the parent window's origin if they can differ.
| const handleRemixingStarted = () => { | ||
| window.top.postMessage( | ||
| { type: "scratch-gui-remixing-started" }, | ||
| window.location.origin, | ||
| process.env.ASSETS_URL, | ||
| ); |
There was a problem hiding this comment.
Same targetOrigin issue as handleUpdateProjectId: this will only work if the parent window's origin matches process.env.ASSETS_URL.
| const handleRemixingSucceeded = () => { | ||
| window.top.postMessage( | ||
| { type: "scratch-gui-remixing-succeeded" }, | ||
| window.location.origin, | ||
| process.env.ASSETS_URL, | ||
| ); |
There was a problem hiding this comment.
Same targetOrigin issue as handleUpdateProjectId: this will only work if the parent window's origin matches process.env.ASSETS_URL.
| const allowedHosts = process.env.ALLOWED_IFRAME_ORIGINS | ||
| ? process.env.ALLOWED_IFRAME_ORIGINS.split(",") |
There was a problem hiding this comment.
ALLOWED_IFRAME_ORIGINS is not exposed to the client-side bundle. The config/env.js file only exposes environment variables that start with REACT_APP_ or are explicitly added to the raw object. This means process.env.ALLOWED_IFRAME_ORIGINS will be undefined at runtime, causing allowedHosts to always be an empty array and all iframe messages to be rejected. Either rename the environment variable to REACT_APP_ALLOWED_IFRAME_ORIGINS or add it explicitly in config/env.js.
| const allowedHosts = process.env.ALLOWED_IFRAME_ORIGINS | |
| ? process.env.ALLOWED_IFRAME_ORIGINS.split(",") | |
| const allowedHosts = process.env.REACT_APP_ALLOWED_IFRAME_ORIGINS | |
| ? process.env.REACT_APP_ALLOWED_IFRAME_ORIGINS.split(",") |
|
@zetter-rpf Please investigate |
Closes: https://github.com/RaspberryPiFoundation/digital-editor-issues/issues/1142
Closes: https://github.com/RaspberryPiFoundation/digital-editor-issues/issues/1161
This makes sure that Scratch can load all the images and JS files it needs.
To do this I decided to host Scratch in an iframe that is served from the code editor site. This simplifies loading assets, latter we will may need need to do some work to make sure the iframe can talk to the API by configuring auth and enabling cross site requests.
I have also changed the webpack config to make other assets available at the correct URLs. See commits for more details.
Note that Sprites and backdrops aren't included in this as they are loaded the same way as project assets
After deploy
ALLOWED_IFRAME_ORIGINSin staging and production. This should be set to the web component url and the editor url.