From 6204a0b1f3bded71fb423e287c03dc514c3f6079 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon <44397098+microbit-matt-hillsdon@users.noreply.github.com> Date: Mon, 15 Apr 2024 12:07:39 +0100 Subject: [PATCH 1/3] Switching from Netlify to CloudFlare for dev (#114) Not really sure this is worth keeping at all but it was the only use of Netlify so migrating to allow us to drop a platform. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cd861fb1..621f04e7 100644 --- a/README.md +++ b/README.md @@ -303,9 +303,9 @@ View at http://localhost:8000/demo.html ### Branch deployments -There is a Netlify based build for development purposes. Do not embed -the simulator via this URL. Netlify's GitHub integration will comment -on PRs with deployment details. +There is a CloudFlare pages based build for development purposes only. Do not +embed the simulator via this URL as it may be removed at any time. CloudFlare's +GitHub integration will comment on PRs with deployment details. Branches in this repository are also deployed via CircleCI to https://review-python-simulator.usermbit.org/{branchName}/. This requires the user pushing code to have permissions for the relevant Micro:bit Educational Foundation infrastructure. From 9d8153846e0262462e99839f49418c3f3a380a65 Mon Sep 17 00:00:00 2001 From: Robert Knight <95928279+microbit-robert@users.noreply.github.com> Date: Tue, 28 May 2024 13:31:17 +0100 Subject: [PATCH 2/3] Add service worker for offline compatibility (#115) For the moment this is opt in via flag=sw on the URL so existing embedders won't be affected. The main motivation here is to experiment with PWA support in micro:bit Python Editor which is being worked on separately. --- .github/workflows/build.yml | 4 +-- Makefile | 2 +- src/Makefile | 3 +- src/environment.ts | 3 ++ src/flags.ts | 56 +++++++++++++++++++++++++++++++++++++ src/simulator.ts | 43 ++++++++++++++++++++++++++++ src/sw.ts | 51 +++++++++++++++++++++++++++++++++ tsconfig.json | 2 +- 8 files changed, 159 insertions(+), 5 deletions(-) create mode 100644 src/environment.ts create mode 100644 src/flags.ts create mode 100644 src/sw.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cba9aca9..232f7a5f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,9 +26,9 @@ jobs: steps: # Note: This workflow will not run on forks without modification; we're open to making steps # that rely on our deployment infrastructure conditional. Please open an issue. - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Configure node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 20.x cache: "npm" diff --git a/Makefile b/Makefile index 60df0f9f..85776152 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ build: dist: build mkdir -p $(BUILD)/build - cp -r $(SRC)/*.html $(SRC)/term.js src/examples $(BUILD) + cp -r $(SRC)/*.html $(SRC)/term.js src/examples $(SRC)/build/sw.js $(BUILD) cp $(SRC)/build/firmware.js $(SRC)/build/simulator.js $(SRC)/build/firmware.wasm $(BUILD)/build/ watch: dist diff --git a/src/Makefile b/src/Makefile index 66dbdcba..90a5effc 100644 --- a/src/Makefile +++ b/src/Makefile @@ -146,7 +146,8 @@ $(BUILD)/micropython.js: $(OBJ) jshal.js simulator-js $(Q)emcc $(LDFLAGS) -o $(BUILD)/firmware.js $(OBJ) $(JSFLAGS) simulator-js: - npx esbuild ./simulator.ts --bundle --outfile=$(BUILD)/simulator.js --loader:.svg=text + npx esbuild '--define:process.env.STAGE="$(STAGE)"' ./simulator.ts --bundle --outfile=$(BUILD)/simulator.js --loader:.svg=text + npx esbuild --define:process.env.VERSION="$$(node -e 'process.stdout.write(`"` + require("../package.json").version + `"`)')" ./sw.ts --bundle --outfile=$(BUILD)/sw.js include $(TOP)/py/mkrules.mk diff --git a/src/environment.ts b/src/environment.ts new file mode 100644 index 00000000..7d295ca7 --- /dev/null +++ b/src/environment.ts @@ -0,0 +1,3 @@ +export type Stage = "local" | "REVIEW" | "STAGING" | "PRODUCTION"; + +export const stage = (process.env.STAGE || "local") as Stage; diff --git a/src/flags.ts b/src/flags.ts new file mode 100644 index 00000000..2e0a517b --- /dev/null +++ b/src/flags.ts @@ -0,0 +1,56 @@ +import { Stage, stage as stageFromEnvironment } from "./environment"; + +/** + * A union of the flag names (alphabetical order). + */ +export type Flag = + /** + * Enables service worker registration. + * + * Registers the service worker and enables offline use. + */ + "sw"; + +interface FlagMetadata { + defaultOnStages: Stage[]; + name: Flag; +} + +const allFlags: FlagMetadata[] = [{ name: "sw", defaultOnStages: [] }]; + +type Flags = Record; + +const flagsForParams = (stage: Stage, params: URLSearchParams) => { + const enableFlags = new Set(params.getAll("flag")); + const allFlagsDefault = enableFlags.has("none") + ? false + : enableFlags.has("*") + ? true + : undefined; + return Object.fromEntries( + allFlags.map((f) => [ + f.name, + isEnabled(f, stage, allFlagsDefault, enableFlags.has(f.name)), + ]) + ) as Flags; +}; + +const isEnabled = ( + f: FlagMetadata, + stage: Stage, + allFlagsDefault: boolean | undefined, + thisFlagOn: boolean +): boolean => { + if (thisFlagOn) { + return true; + } + if (allFlagsDefault !== undefined) { + return allFlagsDefault; + } + return f.defaultOnStages.includes(stage); +}; + +export const flags: Flags = (() => { + const params = new URLSearchParams(window.location.search); + return flagsForParams(stageFromEnvironment, params); +})(); diff --git a/src/simulator.ts b/src/simulator.ts index c45f60e3..7b129130 100644 --- a/src/simulator.ts +++ b/src/simulator.ts @@ -7,6 +7,7 @@ import { createMessageListener, Notifications, } from "./board"; +import { flags } from "./flags"; declare global { interface Window { @@ -15,6 +16,48 @@ declare global { } } +function initServiceWorker() { + window.addEventListener("load", () => { + navigator.serviceWorker.register("sw.js").then( + (registration) => { + console.log("Simulator service worker registration successful"); + // Reload the page when a new service worker is installed. + registration.onupdatefound = function () { + const installingWorker = registration.installing; + if (installingWorker) { + installingWorker.onstatechange = function () { + if ( + installingWorker.state === "installed" && + navigator.serviceWorker.controller + ) { + window.location.reload(); + } + }; + } + }; + }, + (error) => { + console.error(`Simulator service worker registration failed: ${error}`); + } + ); + }); +} + +if ("serviceWorker" in navigator) { + if (flags.sw) { + initServiceWorker(); + } else { + navigator.serviceWorker.getRegistrations().then((registrations) => { + if (registrations.length > 0) { + // We should only have one service worker to unregister. + registrations[0].unregister().then(() => { + window.location.reload(); + }); + } + }); + } +} + const fs = new FileSystem(); const board = createBoard(new Notifications(window.parent), fs); window.addEventListener("message", createMessageListener(board)); diff --git a/src/sw.ts b/src/sw.ts new file mode 100644 index 00000000..2126d359 --- /dev/null +++ b/src/sw.ts @@ -0,0 +1,51 @@ +/// +// Empty export required due to --isolatedModules flag in tsconfig.json +export type {}; +declare const self: ServiceWorkerGlobalScope; +declare const clients: Clients; + +const assets = ["simulator.html", "build/simulator.js", "build/firmware.js", "build/firmware.wasm"]; +const cacheName = `simulator-${process.env.VERSION}`; + +self.addEventListener("install", (event) => { + console.log("Installing simulator service worker..."); + self.skipWaiting(); + event.waitUntil( + (async () => { + const cache = await caches.open(cacheName); + await cache.addAll(assets); + })() + ); +}); + +self.addEventListener("activate", (event) => { + console.log("Activating simulator service worker..."); + event.waitUntil( + (async () => { + const names = await caches.keys(); + await Promise.all( + names.map((name) => { + if (/^simulator-/.test(name) && name !== cacheName) { + return caches.delete(name); + } + }) + ); + await clients.claim(); + })() + ); +}); + +self.addEventListener("fetch", (event) => { + event.respondWith( + (async () => { + const cachedResponse = await caches.match(event.request); + if (cachedResponse) { + return cachedResponse; + } + const response = await fetch(event.request); + const cache = await caches.open(cacheName); + cache.put(event.request, response.clone()); + return response; + })() + ); +}); diff --git a/tsconfig.json b/tsconfig.json index 0716a2c2..40888c08 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "target": "es2019", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": ["dom", "dom.iterable", "esnext", "WebWorker"], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, From 07b1ed74fc1ca1fb3414d22082fccaf63d004441 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon <44397098+microbit-matt-hillsdon@users.noreply.github.com> Date: Wed, 29 May 2024 16:17:08 +0100 Subject: [PATCH 3/3] Change the SW clean-up (#116) We can just remove the SW for our scope. --- src/simulator.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/simulator.ts b/src/simulator.ts index 7b129130..74107222 100644 --- a/src/simulator.ts +++ b/src/simulator.ts @@ -47,13 +47,10 @@ if ("serviceWorker" in navigator) { if (flags.sw) { initServiceWorker(); } else { - navigator.serviceWorker.getRegistrations().then((registrations) => { - if (registrations.length > 0) { - // We should only have one service worker to unregister. - registrations[0].unregister().then(() => { - window.location.reload(); - }); - } + navigator.serviceWorker.getRegistration().then((registration) => { + registration?.unregister().then(() => { + window.location.reload(); + }); }); } }