Skip to content

Interested in AI, automation, blockchain, web and apps

Seoul, KR--:-- GMT
Let’s Talk

Work/Web Development/KO

Online Class Site for Video Lectures and Scratch Practice

Online Class Site for Video Lectures and Scratch Practice

Pulling a scattered Scratch class into one screen

A class spread across three tabs

The job was to move a university's general education Scratch course online. Lecture videos lived on YouTube, and the hands-on work happened in the official Scratch editor.

There were three requirements. First, nobody signs up on their own: only student numbers registered in advance get an account. So there is no sign-up screen at all, and a student can log in only after an administrator pushes their number through addStudentIds. Second, on top of the regular weekly lectures, each student has to be able to get different recommended content. Third, the video and the Scratch editor have to sit on one screen.

The Scratch editor is a large React app built by someone else, and it had to run inside ours.

System boundary and external dependencies
System boundary and external dependencies

One repository, three deployments

The code was split into three modules, app, admin, and gui, using Webpack 5 Module Federation on top of the EMP CLI (@efox/emp-cli)

The student app and the admin app share design tokens, the GraphQL client, and error handling as is, but they ship on different schedules and belong to different people. Bundled into one SPA, a one-line fix on an admin screen means redeploying the student app too As of late 2021 Vite had no usable Module Federation support, and the deployment target was fixed on Vercel

A cookie session was used instead of tokens, and HTTPS was installed locally with mkcert

The remotes load from different origins, so a token in localStorage gives you one copy per origin. Tying it to a single cookie and sending it with credentials: "include" was simpler The price was that local development also had to run on a real domain over HTTPS. That is why adding local.stg-scratch-tutoring.app to /etc/hosts and running sh certificate.sh sits at the very top of the README

Three independent Vercel projects were deployed, and the remote URLs were computed from a per-environment rule

In Module Federation the wiring between modules is ultimately a URL. Splitting it into local / preview / production / ci and letting one getRemoteUrl() decide means even branch previews find each other exactly Because preview URLs derive from the branch name, CI could only run the type check after waiting for all three previews to come up

Deployment and infrastructure
Deployment and infrastructure

A host that is also a remote

The first thing the browser receives is the app bundle. app is a host and a remote at the same time. It exposes the theme, the GraphQL client, error handlers, modals, and icons, and in the other direction it consumes table-shell and table-pagination exposed by admin. The lecture list table on the student home is literally a component that came over from the admin module. This piece covers only how the modules were split and wired, and how someone else's React app was put inside this one. The GraphQL schema contract and code generation, the shared table layer on the admin screens, and the reissue flow when a session expires each deserve their own writeup, so I fold them away here.

Core data model
Core data model

Running a React 16 Scratch editor inside a React 17 app

The usual attempt is one of two things: upgrade scratch-gui to the latest and force it onto the host's React 17, or isolate the whole thing in an iframe. The first drags scratch-vm, scratch-blocks, and scratch-paint along with it, and after burning a few days the render is still broken. The second looks easy, but it cannot satisfy the requirement that the editor and the lecture list rearrange between vertical and horizontal modes inside the same layout, because an iframe does not take part in the parent's flex math.

I pulled the editor out into a separate workspace, packages/gui, and pinned react and react-dom to 16.14.0 inside that package only. Then in the Module Federation shared config I declared this one package as singleton: false. The hosts, app and admin, keep react: { singleton: true }. For production builds, a copy plugin copies scratch-gui's blocks-media, chunks, extension-worker.js, and static/assets into the host's dist so they are served from the same origin. On the component side I wrapped AppStateHOC(Gui) in IntlProvider locale="ko" and pointed the modal root at it with Gui.setAppElement.

singleton: false is a declaration that this dependency may diverge in version and each side should use its own. So a React 16 instance and a React 17 instance are alive on the page at the same time, and Scratch renders its own subtree with its own Redux store and its own React. The two trees never pass props to each other, they meet at a single DOM node, so the rules of hooks never get mixed. And because it is a real DOM element rather than an iframe, the Chakra layout rearranges it freely: a 1024px wide box in vertical mode, a full width box under the video in horizontal mode. Copying the runtime assets into the host dist is the same idea. When the block icons and the extension worker come from the host origin instead of an external CDN, the editor comes up intact even in a classroom with no network.

Two modules that hold each other as remotes

When two apps need to share components, the usual move is to create one private npm package like @company/design-system and install it on both sides.

I opened it in both directions. app exposes core/theme, core/utils/api/client, core/utils/errors/*, and password-field, and registers admin as a remote. admin exposes src/routes, table-shell, and table-pagination, and registers app as a remote. It is a cycle.

The cycle is not a problem at runtime because the emp.js remote entry is lazily loaded. app does not pull in all of admin when it boots. It fetches the one table-shell chunk at the moment the student home draws its table. The cycle exists on the graph only, not in the execution order.

What a user can do

Log in with a student number and password
Log in with a student number and password

Check weekly progress and the lecture list on the home
Check weekly progress and the lecture list on the home

Press resume study to enter the last lecture
Press resume study to enter the last lecture

Rearrange the video and the Scratch editor on the lesson screen
Rearrange the video and the Scratch editor on the lesson screen

Download the lecture materials
Download the lecture materials

Change the password and log in again
Change the password and log in again

Ask an administrator to delete the account
Ask an administrator to delete the account

An administrator logs in and enters the dashboard
An administrator logs in and enters the dashboard

Pick a week and create a regular lecture playlist
Pick a week and create a regular lecture playlist

Paste a YouTube link to register a lecture
Paste a YouTube link to register a lecture

Search, preview, and tidy the recommended lecture list
Search, preview, and tidy the recommended lecture list

Register several student numbers at once
Register several student numbers at once

Pick student accounts and delete them
Pick student accounts and delete them

Approve or deny a student's account deletion request
Approve or deny a student's account deletion request

An administrator logs out
An administrator logs out

1 / 1

Wiring left half finished

To be honest, every screen was drawn but half of it was not wired yet. The week and lecture pickers on the lesson screen are dummies built with Array(50).fill(""), and the six recommended content cards on the right all hold the same string. ModeWork is a single TODO work line, and the admin playlist preview modal plays one hardcoded YouTube URL. The content list screen has a search box and buttons with no query behind them. That is the honest state as of February 2022, when I stepped away.

Opening Module Federation in both directions also cost as much as it gave. CI that waits for three previews is slow, and new people kept asking which package a given component belonged to. If I built it again I would pull the shared UI out into a third remote and have app and admin reference it side by side, removing the cycle. And there is not one line of test. At minimum I should have covered pure functions like useCheckAll and isValidYoutubeUrl.

Read next

Pregnancy and Baby Care Logging App for Couples

Wonderbaby — 2021