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
2 changes: 1 addition & 1 deletion .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ name: CI
on:
push:
branches: [ main ]
# Any base branch, so stacked PRs get CI too
pull_request:
branches: [ main ]

jobs:
e2e:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ name: CI
on:
push:
branches: [ main ]
# Any base branch, so stacked PRs get CI too
pull_request:
branches: [ main ]

jobs:
lint:
Expand Down
65 changes: 65 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Rewrite status, branch/PR stack, roadmap, and known bugs live in the project context
doc, which is imported here. Update that doc (and its "verified as of" line) rather
than duplicating its content in this file.

@docs/PROJECT_CONTEXT.md

## Commands

Python ≥3.14 and Node.js (LTS) are required. Everything runs through uv.

```bash
uv sync # install deps + the mkcd2app entry point
uv run ruff check . # lint
uv run ruff format --check . # format check (drop --check to apply)
uv run mypy . # strict type check
uv run pytest # all tests
uv run pytest tests/e2e/test_statics.py::test_empty_game_build_to_statics_static
```

CI (`lint.yaml`, `e2e.yaml`) runs exactly these lint/format/mypy/pytest commands.
pre-commit runs ruff check, ruff format --check, and mypy on staged Python files.

Running the tool end to end (installs must happen first, in this order):

```bash
uv run mkcd2app toolchain install
uv run mkcd2app target install 4.0.14
uv run mkcd2app --debug build "examples/Racers.yaml"
```

Notes on the e2e tests:
- Run pytest from the repo root: tests use `Path.cwd()` and repo-relative paths, and
shell out to the installed `mkcd2app` executable rather than importing it.
- Builds go to `examples/testing/empty_game_build/`, which the module fixture deletes.

## Architecture

Every unit of work is a redun `@task(namespace="mkcd2app")`. `main.py` parses the CLI
and hands a task expression to `run_redun_task()` with the SQLite DB for that command.
redun builds the DAG from how task results feed into other tasks, so data dependencies
between tasks must go through task arguments/returns, not side channels.

Build pipeline (`build_project/__init__.py`), each step a task:
`fetch_code` → `build_binary_js` (runs `mkc`) + `copy_support_files` (simulator HTML
from the target dir, icon) + `copy_website_template` → `fill_website_template`, plus
`install_website_deps` (`npm ci` from only the template's `package.json` and lockfile)
→ one `build_website*` task per entry in `outputs`. A new output type means
a new `case` arm in that `match` plus a field on `BuildProjectResult`.

Conventions that matter for redun hashing:
- Tasks take the config as the raw YAML string and call `load_config_from_yaml` inside;
never pass pydantic models as task arguments.
- Each stage writes to a fresh directory (named via `_website_dir` in `website.py`) and
returns a `ContentDir`/`ContentFile`; never modify a path another task returned.
- `mkcd2app/__init__.py` imports `_redun_compat` first, which monkey-patches redun and
asserts the pinned version. Changing the redun version means re-checking those patches.

Paths under the user data/state dirs (toolchain, `js_tools/node_modules/.bin` for `mkc`,
npm cache, per-version target dirs, redun DBs) all come from `utils/paths.py`; use those
helpers instead of building paths by hand. Bundled templates and fixtures under
`resources/` are reached through `utils/resources.py`.
124 changes: 124 additions & 0 deletions docs/PROJECT_CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# MakeCode-Arcade-to-App Rewrite — Project Context

_Verified against `toolchain` @ `d7ad64a` plus the `MKCD2APP_HOME` change, the
small known-issue fixes, and the separate deps-install task, and `origin/main` @ `d31e2a5`, as of 2026-10-29._

## Goal
Rewrite `UnsignedArduino/MakeCode-Arcade-to-App` (converts MakeCode Arcade games into a
static website, Electron app, or Tauri app). The original was basically a bash script
written in Python. The rewrite turns it into an incremental build system that runs in CI
and copes with churn in the JS ecosystem.

## Branches / PRs
```
main (legacy) ← PR #6 `dag` (draft) ← PR #7 `toolchain` (draft, base = dag)
```
- The work is on `toolchain`: 53 commits ahead of `main`, 13 ahead of `dag` (`b9c2ab2`)
as of `d7ad64a`.
- `main` has 2 legacy-only fixes (`d17b052`, `d31e2a5`) to files the rewrite deleted.
Merging gives 2 modify/delete conflicts; resolve them by keeping the deletions.
- The legacy Electron/Tauri code to use as reference is on **`main`**
(`src/convert/website_to_{electron,tauri}/`, `src/templates/{electron,tauri}_files/`).
The `electron-output`/`tauri-output` branches are merged and out of date. The legacy
code can't be ported as is; it has to be reimplemented on the new pipeline.

## Layout (`src/mkcd2app/`)
| Path | Purpose |
|---|---|
| `__init__.py` | Applies redun patches, asserts redun version |
| `main.py`, `cli.py` | argparse CLI → redun tasks |
| `_redun_compat.py` | redun monkey-patches |
| `config/`, `models/` | YAML loader; pydantic `BuildConfig`, `BinaryJSMetadata` |
| `build_project/` | `build_project` task, website tasks, code inputs |
| `toolchain/`, `target/` | install/uninstall tasks |
| `utils/` | `run_cmd`, paths (platformdirs), `run_redun_task`, `rmtree_robust`, semver |
| `resources/` | Vite+React+TS template, `mkc` js_tools, empty fixture project |

Other: `tests/e2e/test_statics.py`, `tests/fixtures/`, `examples/` (Racers), CI
workflows `lint.yaml` (ubuntu) and `e2e.yaml` (ubuntu/windows/macos). Tooling: uv,
hatchling, Python ≥3.14, ruff + mypy strict, pre-commit.

## Architecture
- **redun `0.44.1`** (pinned) is the DAG engine, embedded via
`utils/run_redun_task.py`. Each command keeps its own SQLite DB (toolchain, target,
or `build_dir/.redun-cache.db`).
- **Caching is enabled:** an unchanged rebuild is served entirely from the redun cache.
Inputs outside the config aren't tracked, so picking up their changes needs
`--full-rebuild` (see "Out-of-band inputs" under Known issues).
- **Config is passed to tasks as raw YAML text**, because pickling pydantic objects
isn't deterministic and that breaks redun's hashing.
- **Copy-on-write stages:** a path is never written after it's been returned as a
`ContentDir`/`ContentFile` (`x-website` → `-filled` → `-filled-built`).
- **Website builds are offline:** `npm ci --cache <toolchain cache> --offline`.
`install_website_deps` runs it once in `x-website-deps`, hashing only the template's
`package.json`/lockfile, so game and asset changes don't bust it. Each
`build_website*` task copies that `node_modules` into its own copy of the filled site
and runs only `npm run build*`.
- **redun patches** (`_redun_compat.py`): `get_proto` (Windows drive letters),
`ContentDir._calc_hash` and `Dir._calc_hash` (hash file contents, not mtimes). No
tests cover them yet.

## CLI
Global `--debug` (goes before the subcommand). Commands must run in this order:
```
mkcd2app toolchain install # mkc CLI + warm template npm cache
mkcd2app target install <x.y.z> # warm mkc compiler cache + download/inline simulator HTML
mkcd2app build <config.yaml> [--full-rebuild]
```
Also `toolchain uninstall` and `target uninstall <ver>`. `toolchain status` and
`target list` are **stubs**. `build` has **no pre-flight checks**: a missing target
fails with a raw `FileNotFoundError`, a missing toolchain with "Executable 'mkc' not found".

State lives under `platformdirs` user dirs, namespaced by mkcd2app version (static
`0.1.0`). A version bump means re-running both installs. Setting `MKCD2APP_HOME`
moves them to `$MKCD2APP_HOME/{data,state}/<version>` (`utils/paths.py`). `mkc`'s own
compiler/extension caches aren't covered by this.

Config: `version`, `project`, `inputs.code` (share_link | github | path),
`inputs.assets.icon` (optional), `build_dir`, `target` (required, strict `x.y.z`),
`outputs` (static | static-singlefile | electron | tauri).

## Status
- **Done:** `static` and `static-singlefile` outputs; all code sources; toolchain/target
split. Everything that needs the network is vendored at install time **except MakeCode
extensions**, which are fetched per game; `mkc`'s extension cache isn't redirected.
- **Not implemented:** Electron and Tauri. The models and result fields exist, but
`build_project` has no `case` arms for them.
- **E2E test passes** locally (Windows, ~1 min with installs cached) and in CI on
ubuntu/windows/macos (PR #7 @ `d7ad64a`). A session fixture sets `MKCD2APP_HOME` to
`.pytest_cache/d/mkcd2app-home` and runs `toolchain install` and
`target install 4.0.14` there, so the real user dirs are untouched and reruns hit
the install cache. The cache test parses the `ALL` row of the job status table redun
logs to stderr and requires `DONE == 0` and `CACHED == TOTAL` on an unchanged
rebuild; this depends on redun's log format, which is safe while redun is pinned.
Nothing yet tests that changing an input does trigger a rebuild.
- **CI** (`lint.yaml`, `e2e.yaml`) runs on pushes to `main` and on PRs into any branch,
so the stacked PRs get CI. Lint and all three e2e jobs are green on PR #7.
- Only `tests/e2e` exists; there are no unit or integration tests yet.

## Roadmap
1. ~~Fix e2e + CI~~ (done: fixture `target:`, isolated install fixture, caching on,
job-status cache assertion, CI on all PRs).
2. ~~Rename `--clear-cache`~~ (done: now `--full-rebuild`, and its help text says to use
it for out-of-band input changes). Still belongs in the README once that exists.
3. Unit/integration tests, including the redun patches.
4. Electron, then Tauri (reference: `main`).
5. Save-state for Electron/Tauri (investigate `pxtsim` postMessage).
6. Toolchain polish: `status`/`list`, `build` pre-flight, extension cache, version-pinning UX.
7. Later: code signing, auto-update, README (currently a stub), schema docs.

## Known issues
- **Out-of-band inputs (by design):** `fetch_code` and
`copy_support_files` only take the config YAML, so redun can't see changes to a
`path` code source, a `github` branch checkout, an icon path/URL, or the installed
simulator HTML. Editing a path-sourced game returns the old cached build until
`--full-rebuild`. Decided to rely on that flag for all sources rather than track
each one.
- Fixed: the website dep install used to hash the whole filled site, so any asset
change re-ran `npm ci`; it's now its own task (see Architecture).
- Fixed: `download_sim` now decodes downloads as UTF-8 instead of `res.text`, and all
text file reads/writes in `src/` pass `encoding="utf-8"`. (The 4.0.14 simulator was
pure ASCII, so this hadn't caused damage yet.) Also fixed: the `uninstall_toolchain`
docstring, `get_resource_template_path(name)` ignoring `name`,
`load_config_from_yaml` raising `AttributeError` on an unknown version, the stale
`fill_website_template` docstring, and the stray comment in `Racers offline.yaml`.
57 changes: 57 additions & 0 deletions examples/Racers offline.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Example configuration file for MakeCode Arcade to App to compile to static
# HTML, CSS, and JS files

# Config version (single number, incremented if breaking change to schema is
# made)
version: 1

project:
name: Racers
path_friendly_name: racers
description: "Enjoy the high-speed thrills of car racing in MakeCode Arcade!
For the MakeCode Arcade Mini Game Jam #3."
author: Cyrus Yiu
version: 1.3.2
# This is what the window title will be
# This will also be used for the executable file name for Electron outputs
# You can use {NAME} or {VERSION} or {AUTHOR} to substitute the correct
# values
title: "{NAME} v{VERSION}"

inputs:
# Can be share_link, github, or path
code:
type: path
value: "E:/Racers"

assets:
# Icon is optional but highly recommended, it will be used as the favicon
# and app icon
icon:
type: path
value: "E:/Racers/Racers icon.png"

# Path where the build process will take place
build_dir: "./examples/Racers-offline-build"

# The MakeCode Arcade version to target
# Must be explicit 3 number sem ver, do not add a "v" at the beginning
# To find it, go to https://arcade.makecode.com/ and click on the settings
# cogwheel button, click About..., and see "arcade version: x.y.z"
target: 2.0.48

# You can have multiple outputs
# Currently available options are static, static-singlefile, electron, and
# tauri
outputs:
- type: static
- type: static-singlefile
# - type: electron
# window:
# width: 640
# height: 480
# - type: tauri
# identifier: com.unsignedarduino.racers
# window:
# width: 640
# height: 480
7 changes: 7 additions & 0 deletions examples/Racers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ inputs:
# Path where the build process will take place
build_dir: "./examples/Racers-build"

# The MakeCode Arcade version to target
# Must be explicit 3 number sem ver, do not add a "v" at the beginning
# To find it, go to https://arcade.makecode.com/ and click on the settings
# cogwheel button, click About..., and see "arcade version: x.y.z"
# As of the time of writing this, the latest version is 4.0.14
target: 4.0.14

# You can have multiple outputs
# Currently available options are static, static-singlefile, electron, and
# tauri
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dependencies = [
"requests",
"beautifulsoup4",
"pillow",
"platformdirs>=4.11.0",
]

[project.scripts]
Expand Down
Loading
Loading