Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions config/buildArtifacts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
const path = require("path");

const getScratchStaticDir = (rootDir) =>
path.resolve(
rootDir,
"node_modules/@RaspberryPiFoundation/scratch-gui/dist/static",
);

const getScratchChunkDir = (rootDir) =>
path.resolve(
rootDir,
"node_modules/@RaspberryPiFoundation/scratch-gui/dist/chunks",
);

const mainCopyPatterns = [
{ from: "public", to: "" },
{ from: "src/projects", to: "projects" },
{
from: "node_modules/@raspberrypifoundation/python-friendly-error-messages/copydecks",
to: "python-error-copydecks",
},
];

const getScratchCopyPatterns = ({ scratchStaticDir, scratchChunkDir }) => [
{ from: scratchStaticDir, to: "scratch-gui/static" },
{ from: `${scratchStaticDir}/assets`, to: "vendor/static/assets" },
{ from: scratchChunkDir, to: "chunks" },
{
from: "node_modules/scratchReactVendor/umd/react.production.min.js",
to: "vendor/react.production.min.js",
},
{
from: "node_modules/scratchReactDomVendor/umd/react-dom.production.min.js",
to: "vendor/react-dom.production.min.js",
},
{
from: "node_modules/redux/dist/redux.min.js",
to: "vendor/redux.min.js",
},
{
from: "node_modules/react-redux/dist/react-redux.min.js",
to: "vendor/react-redux.min.js",
},
{
from: "node_modules/@RaspberryPiFoundation/scratch-gui/dist/scratch-gui.js",
to: "vendor/scratch-gui.js",
},
{
from: "node_modules/@RaspberryPiFoundation/scratch-gui/dist/scratch-gui.js.LICENSE.txt",
to: "vendor/scratch-gui.js.LICENSE.txt",
},
];

module.exports = {
getScratchChunkDir,
getScratchCopyPatterns,
getScratchStaticDir,
mainCopyPatterns,
};
37 changes: 37 additions & 0 deletions config/devServerSecurity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const crossOriginHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
"Access-Control-Allow-Headers":
"X-Requested-With, content-type, Authorization",
// Pyodide input and interruption need cross-origin isolation on the host app.
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
};

const crossOriginResourcePolicyExactPaths = [
"/pyodide/shims/_internal_sense_hat.js",
"/pyodide/shims/pygal.js",
"/PyodideWorker.js",
];

const crossOriginResourcePolicyPrefixes = [
"/scratch.html",
"/html-renderer.html",
];

const shouldSetCrossOriginResourcePolicy = (url = "") =>
crossOriginResourcePolicyExactPaths.includes(url) ||
crossOriginResourcePolicyPrefixes.some((prefix) => url.startsWith(prefix));

const setCrossOriginResourcePolicy = (req, res, next) => {
if (shouldSetCrossOriginResourcePolicy(req.url)) {
res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
}
next();
};

module.exports = {
crossOriginHeaders,
setCrossOriginResourcePolicy,
shouldSetCrossOriginResourcePolicy,
};
13 changes: 13 additions & 0 deletions docs/ViteMigration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Vite migration notes

## Test runner scope

Keep Jest as the test runner for the initial Vite bundler migration.

Jest is already wired into local scripts, CI coverage reporting, React test
setup, CSS/SVG transforms, and worker mocks. Migrating to Vitest at the same
time would make bundler regressions harder to identify because test
infrastructure changes would be mixed with runtime build changes.

After the Vite build is deployed, Vitest can be evaluated as a separate change
with its own coverage and transform parity checks.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"scripts": {
"start": "NODE_ENV=development BABEL_ENV=development webpack serve -c ./webpack.config.js",
"build": "NODE_ENV=production BABEL_ENV=production webpack build -c ./webpack.config.js",
"check-build-artifacts": "node scripts/check-build-artifacts.js",
"analyze": "ANALYZE_WEBPACK_BUNDLE=true yarn build",
"lint": "eslint 'src/**/*.{js,jsx}' cypress/**/*.js",
"lint:fix": "eslint --fix 'src/**/*.{js,jsx}' cypress/**/*.js",
Expand Down Expand Up @@ -198,6 +199,8 @@
"modulePaths": [],
"moduleNameMapper": {
"^react-native$": "react-native-web",
"^(.+\\.md)\\?raw$": "$1",
"^(.+\\.(css|sass|scss))\\?inline$": "$1",
"^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy"
},
"moduleFileExtensions": [
Expand Down
44 changes: 44 additions & 0 deletions scripts/check-build-artifacts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
const fs = require("fs");
const path = require("path");

const buildDir = path.resolve(__dirname, "..", "build");

const requiredFiles = [
"web-component.html",
"web-component.js",
"html-renderer.html",
"html-renderer.js",
"scratch.html",
"scratch.js",
"PyodideWorker.js",
"translations/en.json",
"translations/xx-XX.json",
"projects/blank-html-starter.json",
"projects/cool-python.json",
"pyodide/shims/_internal_sense_hat.js",
"pyodide/shims/pygal.js",
"shims/processing/p5/p5-shim.js",
"shims/sense_hat/sense_hat_blob.py",
"python-error-copydecks/en/copydeck.json",
"scratch-gui/static/assets",
"chunks",
"vendor/react.production.min.js",
"vendor/react-dom.production.min.js",
"vendor/redux.min.js",
"vendor/react-redux.min.js",
"vendor/scratch-gui.js",
];

const missing = requiredFiles.filter(
(relativePath) => !fs.existsSync(path.join(buildDir, relativePath)),
);

if (missing.length > 0) {
console.error("Missing required build artifacts:");
missing.forEach((relativePath) => console.error(`- ${relativePath}`));
process.exit(1);
}

console.log(
`Found ${requiredFiles.length} required build artifacts in ${buildDir}.`,
);
15 changes: 7 additions & 8 deletions src/PyodideWorker.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
/* global globalThis, importScripts, loadPyodide, SharedArrayBuffer, Atomics, pygal, _internal_sense_hat */
import { assetPath } from "./utils/runtimeConfig";

// Nest the PyodideWorker function inside a globalThis object so we control when its initialised.
const PyodideWorker = () => {
// Import scripts dynamically based on the environment
importScripts(
`${process.env.ASSETS_URL}/pyodide/shims/_internal_sense_hat.js`,
);
importScripts(`${process.env.ASSETS_URL}/pyodide/shims/pygal.js`);
importScripts(assetPath("pyodide/shims/_internal_sense_hat.js"));
importScripts(assetPath("pyodide/shims/pygal.js"));
importScripts("https://cdn.jsdelivr.net/pyodide/v0.26.2/full/pyodide.js");

const supportsAllFeatures = typeof SharedArrayBuffer !== "undefined";
Expand Down Expand Up @@ -202,7 +201,7 @@ const PyodideWorker = () => {
enigma: {
before: async () => {
await pyodide.loadPackage(
`${process.env.ASSETS_URL}/pyodide/packages/py_enigma-0.1-py3-none-any.whl`,
assetPath("pyodide/packages/py_enigma-0.1-py3-none-any.whl"),
);
},
after: () => {},
Expand All @@ -211,7 +210,7 @@ const PyodideWorker = () => {
before: async () => {
pyodide.registerJsModule("basthon", fakeBasthonPackage);
await pyodide.loadPackage(
`${process.env.ASSETS_URL}/pyodide/packages/turtle-0.0.1-py3-none-any.whl`,
assetPath("pyodide/packages/turtle-0.0.1-py3-none-any.whl"),
);
},
after: () =>
Expand All @@ -229,7 +228,7 @@ const PyodideWorker = () => {
pyodide.registerJsModule("basthon", fakeBasthonPackage);
await pyodide.loadPackage([
"setuptools",
`${process.env.ASSETS_URL}/pyodide/packages/p5-0.0.1-py3-none-any.whl`,
assetPath("pyodide/packages/p5-0.0.1-py3-none-any.whl"),
]);
},
after: () => {},
Expand All @@ -250,7 +249,7 @@ const PyodideWorker = () => {
});
await pyodide.loadPackage([
"pillow",
`${process.env.ASSETS_URL}/pyodide/packages/sense_hat-0.0.1-py3-none-any.whl`,
assetPath("pyodide/packages/sense_hat-0.0.1-py3-none-any.whl"),
]);

_internal_sense_hat.config.pyodide = pyodide;
Expand Down
5 changes: 2 additions & 3 deletions src/components/AstroPiModel/FlightCase.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@ import * as THREE from "three";
import { useGLTF } from "@react-three/drei";
import Sk from "skulpt";
import { invalidate } from "@react-three/fiber";
import { assetPath } from "../../utils/runtimeConfig";

const FlightCase = () => {
const { scene } = useGLTF(
`${process.env.ASSETS_URL}/models/raspi-compressed.glb`,
);
const { scene } = useGLTF(assetPath("models/raspi-compressed.glb"));
window.mod = scene;

const blankLED = new THREE.MeshStandardMaterial({ color: `rgb(0,0,0)` });
Expand Down
5 changes: 2 additions & 3 deletions src/components/Editor/Project/ScratchContainer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
postMessageToScratchIframe,
getScratchAllowedOrigin,
} from "../../../utils/scratchIframe";
import { assetPath } from "../../../utils/runtimeConfig";

const SCRATCH_MIN_WIDTH = 1024;
const SCRATCH_SCROLLBAR_OPTIONS = {
Expand Down Expand Up @@ -126,9 +127,7 @@ export default function ScratchContainer() {
queryParams.set("scratchMetadata", "1");
queryParams.set("parent_origin", window.location.origin);

const iframeSrcUrl = `${
process.env.ASSETS_URL
}/scratch.html?${queryParams.toString()}`;
const iframeSrcUrl = assetPath(`scratch.html?${queryParams.toString()}`);

return (
<div className="scratch-container" data-testid="scratch-container">
Expand Down
10 changes: 6 additions & 4 deletions src/components/Editor/Runners/HtmlRunner/HtmlRenderer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import {
allowedInternalLinks,
matchingRegexes,
} from "../../../../utils/externalLinkHelper";
import htmlRunnerStyles from "../../../../assets/stylesheets/HtmlRunner.scss";
import htmlRunnerStyles from "../../../../assets/stylesheets/HtmlRunner.scss?inline";
import {
allowedIframeHost,
MSG_HTML_PREVIEW_EVENT,
MSG_HTML_PREVIEW_READY,
MSG_HTML_PROJECT_UPDATE,
} from "../../../../utils/iframeUtils";
import { getHtmlRendererUrl } from "../../../../utils/runtimeConfig";

const parentTag = (node, tag) =>
node.parentNode?.tagName && node.parentNode.tagName.toLowerCase() === tag;
Expand All @@ -39,6 +40,7 @@ const getBlobURL = (code, type) => {

const replaceHrefNodes = (indexPage, projectMedia, projectCode) => {
const hrefNodes = indexPage.querySelectorAll("[href]");
const htmlRendererUrl = getHtmlRendererUrl();

hrefNodes.forEach((hrefNode) => {
const projectFile = projectCode.find(
Expand All @@ -61,7 +63,7 @@ const replaceHrefNodes = (indexPage, projectMedia, projectCode) => {
} else {
// eslint-disable-next-line no-script-url
hrefNode.setAttribute("href", "javascript:void(0)");
onClick = `window.parent.postMessage({type: '${MSG_HTML_PREVIEW_EVENT}', msg: 'RELOAD', payload: { linkTo: '${projectFile.name}' }}, '${process.env.HTML_RENDERER_URL}')`;
onClick = `window.parent.postMessage({type: '${MSG_HTML_PREVIEW_EVENT}', msg: 'RELOAD', payload: { linkTo: '${projectFile.name}' }}, '${htmlRendererUrl}')`;
}
} else {
const matchingExternalHref = matchingRegexes(
Expand All @@ -79,9 +81,9 @@ const replaceHrefNodes = (indexPage, projectMedia, projectCode) => {
) {
// eslint-disable-next-line no-script-url
hrefNode.setAttribute("href", "javascript:void(0)");
onClick = `window.parent.postMessage({type: '${MSG_HTML_PREVIEW_EVENT}', msg: 'ERROR: External link'}, '${process.env.HTML_RENDERER_URL}')`;
onClick = `window.parent.postMessage({type: '${MSG_HTML_PREVIEW_EVENT}', msg: 'ERROR: External link'}, '${htmlRendererUrl}')`;
} else if (matchingExternalHref) {
onClick = `window.parent.postMessage({type: '${MSG_HTML_PREVIEW_EVENT}', msg: 'Allowed external link', payload: { linkTo: '${hrefNode.attrs.href}' }}, '${process.env.HTML_RENDERER_URL}')`;
onClick = `window.parent.postMessage({type: '${MSG_HTML_PREVIEW_EVENT}', msg: 'Allowed external link', payload: { linkTo: '${hrefNode.attrs.href}' }}, '${htmlRendererUrl}')`;
}
}

Expand Down
16 changes: 12 additions & 4 deletions src/components/Editor/Runners/HtmlRunner/HtmlRenderer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
MSG_HTML_PREVIEW_READY,
MSG_HTML_PROJECT_UPDATE,
} from "../../../../utils/iframeUtils";
import { getHtmlRendererUrl } from "../../../../utils/runtimeConfig";

let mockMediaQuery = (query) => {
return matchMedia(query).matches;
Expand Down Expand Up @@ -85,7 +86,10 @@ const forbiddenExternalLinkHTMLPage = {
};

describe("When run is triggered", () => {
const originalHtmlRendererUrl = process.env.HTML_RENDERER_URL;

beforeEach(async () => {
process.env.HTML_RENDERER_URL = "https://renderer.example.com";
let ready = false;

const onMessage = (event) => {
Expand All @@ -104,6 +108,10 @@ describe("When run is triggered", () => {
window.removeEventListener("message", onMessage);
});

afterEach(() => {
process.env.HTML_RENDERER_URL = originalHtmlRendererUrl;
});

describe("When basic HTML is rendered", () => {
beforeEach(() => {
window.postMessage(
Expand Down Expand Up @@ -147,7 +155,7 @@ describe("When run is triggered", () => {
'<a href="javascript:void(0)"',
);
expect(iframe.getAttribute("srcdoc")).toContain(
`onclick="window.parent.postMessage({type: 'editor-html-event', msg: 'ERROR: External link'}, '${process.env.HTML_RENDERER_URL}')"`,
`onclick="window.parent.postMessage({type: 'editor-html-event', msg: 'ERROR: External link'}, '${getHtmlRendererUrl()}')"`,
);
expect(iframe.getAttribute("srcdoc")).toContain("EXTERNAL LINK!");
});
Expand Down Expand Up @@ -176,7 +184,7 @@ describe("When run is triggered", () => {
'<a href="javascript:void(0)"',
);
expect(iframe.getAttribute("srcdoc")).toContain(
`onclick="window.parent.postMessage({type: 'editor-html-event', msg: 'RELOAD', payload: { linkTo: 'index' }}, '${process.env.HTML_RENDERER_URL}')"`,
`onclick="window.parent.postMessage({type: 'editor-html-event', msg: 'RELOAD', payload: { linkTo: 'index' }}, '${getHtmlRendererUrl()}')"`,
);
expect(iframe.getAttribute("srcdoc")).toContain("NEW TAB LINK!");
});
Expand Down Expand Up @@ -204,7 +212,7 @@ describe("When run is triggered", () => {
'<a href="javascript:void(0)"',
);
expect(iframe.getAttribute("srcdoc")).toContain(
`onclick="window.parent.postMessage({type: 'editor-html-event', msg: 'RELOAD', payload: { linkTo: 'test' }}, '${process.env.HTML_RENDERER_URL}')`,
`onclick="window.parent.postMessage({type: 'editor-html-event', msg: 'RELOAD', payload: { linkTo: 'test' }}, '${getHtmlRendererUrl()}')`,
);
expect(iframe.getAttribute("srcdoc")).toContain("ANCHOR LINK!");
});
Expand Down Expand Up @@ -232,7 +240,7 @@ describe("When run is triggered", () => {
'<a href="https://rpf.io/seefood"',
);
expect(iframe.getAttribute("srcdoc")).toContain(
`onclick="window.parent.postMessage({type: 'editor-html-event', msg: 'Allowed external link', payload: { linkTo: 'https://rpf.io/seefood' }}, '${process.env.HTML_RENDERER_URL}')`,
`onclick="window.parent.postMessage({type: 'editor-html-event', msg: 'Allowed external link', payload: { linkTo: 'https://rpf.io/seefood' }}, '${getHtmlRendererUrl()}')`,
);
expect(iframe.getAttribute("srcdoc")).toContain("RPF link");
});
Expand Down
8 changes: 6 additions & 2 deletions src/components/Editor/Runners/HtmlRunner/HtmlRunner.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ import {
MSG_HTML_PREVIEW_READY,
MSG_HTML_PROJECT_UPDATE,
} from "../../../../utils/iframeUtils";
import {
getHtmlRendererUrl,
htmlRendererPath,
} from "../../../../utils/runtimeConfig";

function HtmlRunner() {
const project = useSelector((state) => state.editor.project);
Expand Down Expand Up @@ -200,7 +204,7 @@ function HtmlRunner() {
media: projectMedia,
current: indexPage.toString(),
},
process.env.HTML_RENDERER_URL,
getHtmlRendererUrl(),
);

if (codeRunTriggered) {
Expand Down Expand Up @@ -260,7 +264,7 @@ function HtmlRunner() {
id="output-frame"
title={t("runners.HtmlOutput")}
ref={output}
src={`${process.env.HTML_RENDERER_URL}/html-renderer.html`}
src={htmlRendererPath("html-renderer.html")}
onLoad={() => {
setExternalLink(null);
}}
Expand Down
Loading
Loading