diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 1eabf0edd..750965eef 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -1,5 +1,9 @@ name: CI-CD +permissions: + contents: read + packages: read + on: push: branches: diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b9e916af8..1f4931ced 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,5 +1,9 @@ name: Deploy +permissions: + contents: read + packages: read + on: workflow_call: inputs: diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..5cac20b35 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +@RaspberryPiFoundation:registry=https://npm.pkg.github.com +//npm.pkg.github.com/:_authToken=${NPM_AUTH_TOKEN} diff --git a/.yarnrc.yml b/.yarnrc.yml index 7b135dac3..e69932c71 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -12,4 +12,5 @@ npmMinimalAgeGate: 7d npmPreapprovedPackages: - "@raspberrypifoundation/design-system-react" - - "@raspberrypifoundation/design-system-core" \ No newline at end of file + - "@raspberrypifoundation/design-system-core" + - "@raspberrypifoundation/scratch-gui" \ No newline at end of file diff --git a/README.md b/README.md index 5d20b0ed8..138aba606 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,42 @@ This project provides a web component containing the Raspberry Pi Code Editor for use on other sites. Although originally bootstrapped with [Create React App](https://github.com/facebook/create-react-app), the application has been ejected so all the build scripts etc. are now in the repo. +## GitHub Packages authentication + +Some dependencies are published to [GitHub Packages](https://github.com/orgs/RaspberryPiFoundation/packages), including: + +- [`@RaspberryPiFoundation/scratch-gui`](https://github.com/RaspberryPiFoundation/scratch-editor/pkgs/npm/scratch-gui) (Scratch editor UI) +- [`@raspberrypifoundation/design-system-react`](https://github.com/RaspberryPiFoundation/design-system-react) + +1. Create a GitHub personal access token with the `read:packages` scope. +2. Export it for Yarn (the committed `.npmrc` reads this variable): + + ```bash + export NPM_AUTH_TOKEN= + ``` + +3. Install dependencies with Yarn 4: + + ```bash + yarn install + ``` + +CI uses `GITHUB_TOKEN` automatically; you only need `NPM_AUTH_TOKEN` for local development (and Docker — see below). + +**Scratch GUI version:** `package.json` pins `@RaspberryPiFoundation/scratch-gui` to a GitHub Packages release (e.g. `13.7.3-experience-cs.`). Until that package is published: + +- Do **not** run `yarn install` expecting the pin to resolve — `yarn.lock` is unchanged and CI `yarn install --immutable` will fail until you replace `PLACEHOLDER` with the real version from [scratch-gui packages](https://github.com/RaspberryPiFoundation/scratch-editor/pkgs/npm/scratch-gui), run `yarn install`, and commit `yarn.lock`. +- To work on scratch-gui **before** publish, use the [local scratch-editor file link](#linking-a-local-scratch-editor-scratch-gui) below instead of the registry pin. + ## Install dependencies -This repository uses Yarn (see `package.json` → `packageManager`). Please install dependencies with Yarn: +This repository uses Yarn 4 (see `package.json` → `packageManager` and `.yarnrc.yml`). Install dependencies with: ``` yarn install ``` -Using `npm install` can fail due to strict peer-dependency resolution in npm for some legacy packages in this project. +Do not use `npm install` — it can fail due to strict peer-dependency resolution for some legacy packages in this project. ## Environment variables @@ -227,6 +254,93 @@ Python code snippets are styled and syntax-highlighted using the `language-pytho print('hello world') ``` +### Linking a local scratch-editor (Scratch GUI) + +When you are working on the [Raspberry Pi Foundation scratch-editor](https://github.com/RaspberryPiFoundation/scratch-editor) fork (for example changes to `scratch-gui`), you may want editor-ui to load that build instead of the published `@RaspberryPiFoundation/scratch-gui` package from GitHub Packages. + +editor-ui does not bundle Scratch GUI into the main webpack app. It copies a prebuilt `dist/scratch-gui.js` (and static assets) from `node_modules` into the dev server output. Pointing the dependency at your local clone is enough to test GUI changes in the Scratch iframe. + +**Do not commit these linking changes.** They are temporary for local development only. These changes to `package.json`, `yarn.lock`, and `docker-compose.yml` are for local development only. + +#### Repository layout + +Clone both repositories as siblings: + +```text +Development/ + editor-ui/ + scratch-editor/ ← github.com/RaspberryPiFoundation/scratch-editor +``` + +#### Temporary changes in editor-ui + +**1. `package.json`** — replace the registry dependency with a file link: + +```json +"@RaspberryPiFoundation/scratch-gui": "file:../scratch-editor/packages/scratch-gui" +``` + +**2. `docker-compose.yml`** — mount the scratch-editor repo into the container (read-only): + +```yaml +volumes: + - .:/app + - ../scratch-editor:/scratch-editor:ro + - node_modules:/app/node_modules +``` + +From `/app` in the container, `file:../scratch-editor/packages/scratch-gui` resolves to `/scratch-editor/packages/scratch-gui` on the mounted volume. + +**3. `yarn.lock`** — run `yarn install` inside Docker (see below) and commit nothing; the lockfile will change while the link is active. Revert it when you restore the registry version in `package.json`. + +The file link uses the same package name as the published dependency (`@RaspberryPiFoundation/scratch-gui`). GitHub Packages authentication is not required for scratch-gui while the file link is active (you may still need `NPM_AUTH_TOKEN` for other dependencies). + +#### Build scratch-gui + +Build on your machine (the `scratch-editor` mount is read-only inside the editor-ui container): + +```bash +cd ../scratch-editor +npm ci +npm run build -w @RaspberryPiFoundation/scratch-gui +``` + +After every scratch-gui code change, run the build again, then restart the editor-ui container so webpack copies the new `dist/scratch-gui.js`. + +#### Run with Docker + +**editor-api** (Scratch projects and assets API): + +```bash +cd ../editor-api +docker compose up +``` + +**editor-ui**: + +```bash +cd ../editor-ui +docker compose up +``` + +The container runs `yarn install` then `yarn start` on each start. Pass `NPM_AUTH_TOKEN` on the host (see [GitHub Packages authentication](#github-packages-authentication)) so the container can install dependencies from GitHub Packages. The first start after switching to the file link may take longer while dependencies are linked. + +#### Verify in the browser + +Scratch runs in an iframe served from editor-ui (port **3011**), not from the main web-component bundle alone. + +- Web component test page: open a **Scratch** sample, e.g. + `http://localhost:3011/web-component.html` + then choose **cool-scratch**. +- **editor-standalone** (port **3012**): also loads the web component from `http://localhost:3011`; open a Scratch project under `/en-US/projects/…` with editor-ui and editor-api running. + +#### Revert when finished + +1. Restore the published `@RaspberryPiFoundation/scratch-gui` version pin in `package.json` (see [GitHub Packages authentication](#github-packages-authentication)). +2. Remove the `../scratch-editor:/scratch-editor:ro` volume from `docker-compose.yml`. +3. Revert `yarn.lock` (e.g. `git checkout -- yarn.lock`) or run `yarn install` again after restoring `package.json`. +4. Restart `docker compose up` in editor-ui. + ## Deployment Deployment is managed through Github actions. The UI is deployed to staging and production environments through an S3 bucket, managed via Cloudflare. This requires the following environment variables to be set diff --git a/docker-compose.yml b/docker-compose.yml index d1ecbd1ff..ce87e03d4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,8 @@ x-app: &x-app build: context: . + environment: + NPM_AUTH_TOKEN: ${NPM_AUTH_TOKEN} volumes: - .:/app - node_modules:/app/node_modules diff --git a/package.json b/package.json index f576d96bd..543a41f09 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@react-three/fiber": "^8.0.13", "@reduxjs/toolkit": "^1.6.2", "@replit/codemirror-indentation-markers": "^6.1.0", - "@scratch/scratch-gui": "^13.0.0", + "@RaspberryPiFoundation/scratch-gui": "13.7.3-experience-cs.PLACEHOLDER", "@sentry/browser": "^7.17.3", "@sentry/react": "7.16.0", "@sentry/tracing": "7.16.0", diff --git a/src/components/ScratchEditor/ScratchEditor.jsx b/src/components/ScratchEditor/ScratchEditor.jsx index dd29f75c3..64110f9bd 100644 --- a/src/components/ScratchEditor/ScratchEditor.jsx +++ b/src/components/ScratchEditor/ScratchEditor.jsx @@ -79,6 +79,8 @@ const ScratchEditor = ({ menuBarHidden={true} projectHost={`${apiUrl}/api/scratch/projects`} assetHost={`${apiUrl}/api/scratch/assets`} + libraryAssetHost={`${apiUrl}/api/scratch/assets`} + libraryAssetsFetchWithHeaders basePath={`${process.env.ASSETS_URL}/scratch-gui/`} onStorageInit={(storage) => { scratchFetchApiRef.current = storage.scratchFetch; diff --git a/src/components/ScratchEditor/ScratchEditor.test.jsx b/src/components/ScratchEditor/ScratchEditor.test.jsx index 30d4fd320..42956521c 100644 --- a/src/components/ScratchEditor/ScratchEditor.test.jsx +++ b/src/components/ScratchEditor/ScratchEditor.test.jsx @@ -32,6 +32,14 @@ describe("ScratchEditor", () => { ); expect(getByTestId("wrapped-scratch-gui")).toBeTruthy(); + expect(mockWrappedScratchGui).toHaveBeenCalledWith( + expect.objectContaining({ + assetHost: "https://api.example.com/api/scratch/assets", + libraryAssetHost: "https://api.example.com/api/scratch/assets", + libraryAssetsFetchWithHeaders: true, + projectHost: "https://api.example.com/api/scratch/projects", + }), + ); }); test("routes project saves through scratchFetch metadata after storage init", async () => { diff --git a/src/components/ScratchEditor/ScratchIntegrationHOC.jsx b/src/components/ScratchEditor/ScratchIntegrationHOC.jsx index 34b257565..a6cb74c51 100644 --- a/src/components/ScratchEditor/ScratchIntegrationHOC.jsx +++ b/src/components/ScratchEditor/ScratchIntegrationHOC.jsx @@ -6,7 +6,7 @@ import { remixProject, manualUpdateProject, setStageSize, -} from "@scratch/scratch-gui"; +} from "@RaspberryPiFoundation/scratch-gui"; import { allowedIframeHost } from "../../utils/iframeUtils"; import { postScratchGuiEvent } from "./events.js"; diff --git a/src/components/ScratchEditor/ScratchIntegrationHOC.test.jsx b/src/components/ScratchEditor/ScratchIntegrationHOC.test.jsx index 516cca6fd..d1f5072ff 100644 --- a/src/components/ScratchEditor/ScratchIntegrationHOC.test.jsx +++ b/src/components/ScratchEditor/ScratchIntegrationHOC.test.jsx @@ -5,7 +5,7 @@ const configureStore = require("redux-mock-store").default; jest.mock("file-saver", () => ({ saveAs: jest.fn() })); jest.mock("./events.js", () => ({ postScratchGuiEvent: jest.fn() })); -jest.mock("@scratch/scratch-gui", () => ({ +jest.mock("@RaspberryPiFoundation/scratch-gui", () => ({ remixProject: () => ({ type: "remix" }), manualUpdateProject: () => ({ type: "manualUpdate" }), setStageSize: () => ({ type: "setStageSize" }), diff --git a/src/components/ScratchEditor/WrappedScratchGui.jsx b/src/components/ScratchEditor/WrappedScratchGui.jsx index 5f30e29cd..f8e647766 100644 --- a/src/components/ScratchEditor/WrappedScratchGui.jsx +++ b/src/components/ScratchEditor/WrappedScratchGui.jsx @@ -1,4 +1,4 @@ -import GUI, { AppStateHOC } from "@scratch/scratch-gui"; +import GUI, { AppStateHOC } from "@RaspberryPiFoundation/scratch-gui"; import ScratchIntegrationHOC from "./ScratchIntegrationHOC.jsx"; import { compose } from "redux"; diff --git a/src/scratch.html b/src/scratch.html index 609a8734d..ffbb657dd 100644 --- a/src/scratch.html +++ b/src/scratch.html @@ -14,8 +14,8 @@ worker-src 'self' blob:; child-src 'self' blob:; connect-src 'self' <%= cspApiMultipleOrigins || cspApiOrigin %> <%= cspAssetOrigin %> <%= isDev ? "ws: wss:" : "" %>; - img-src 'self' data: blob: <%= cspAssetOrigin %>; - media-src 'self' blob: <%= cspAssetOrigin %>; + img-src 'self' data: blob: <%= cspApiMultipleOrigins || cspApiOrigin %> <%= cspAssetOrigin %>; + media-src 'self' blob: <%= cspApiMultipleOrigins || cspApiOrigin %> <%= cspAssetOrigin %>; font-src 'self' data: <%= cspAssetOrigin %>; form-action 'self'; upgrade-insecure-requests; diff --git a/webpack.config.js b/webpack.config.js index 2a95ff47b..8080920f3 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -50,12 +50,12 @@ const cspApiMultipleOrigins = String(process.env.CSP_API_MULTIPLE_ORIGINS || "") const scratchStaticDir = path.resolve( __dirname, - "node_modules/@scratch/scratch-gui/dist/static", + "node_modules/@RaspberryPiFoundation/scratch-gui/dist/static", ); const scratchChunkDir = path.resolve( __dirname, - "node_modules/@scratch/scratch-gui/dist/chunks", + "node_modules/@RaspberryPiFoundation/scratch-gui/dist/chunks", ); const moduleRules = [ @@ -255,7 +255,8 @@ const scratchConfig = { }, externals: [ function ({ request }, callback) { - if (request === "@scratch/scratch-gui") return callback(null, "GUI"); + if (request === "@RaspberryPiFoundation/scratch-gui") + return callback(null, "GUI"); if (request === "react") return callback(null, "React"); if (request === "react-dom" || request.startsWith("react-dom/")) return callback(null, "ReactDOM"); @@ -304,11 +305,11 @@ const scratchConfig = { to: "vendor/react-redux.min.js", }, { - from: "node_modules/@scratch/scratch-gui/dist/scratch-gui.js", + from: "node_modules/@RaspberryPiFoundation/scratch-gui/dist/scratch-gui.js", to: "vendor/scratch-gui.js", }, { - from: "node_modules/@scratch/scratch-gui/dist/scratch-gui.js.LICENSE.txt", + from: "node_modules/@RaspberryPiFoundation/scratch-gui/dist/scratch-gui.js.LICENSE.txt", to: "vendor/scratch-gui.js.LICENSE.txt", }, ],