From 263d797b6d43d36fd732d43b062a0a3edcf0c53d Mon Sep 17 00:00:00 2001 From: nsprenkle Date: Mon, 9 Mar 2026 10:49:16 -0400 Subject: [PATCH 01/11] feat: add initial CLAUDE.md --- CLAUDE.md | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..fd007bc171 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,112 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +```bash +# Development +npm start # Start dev server (standard) +npm run dev # Start dev server with local OpenEdX config (host: apps.local.openedx.io) +npm run build # Production webpack build + +# Testing +npm test # Run all tests with coverage +npm run test:watch # Watch mode +npm run snapshot # Update snapshots + +# Run a single test file +NODE_ENV=test npx jest path/to/file.test.jsx + +# Linting +npm run lint # Check +npm run lint:fix # Auto-fix + +# Type checking +npm run types # tsc --noEmit (TypeScript only) + +# i18n +npm run i18n_extract # Extract translation strings +``` + +## Architecture + +This is an OpenEdX Micro-Frontend (MFE) built on `@edx/frontend-platform` and `@openedx/frontend-build`. It serves the learner-facing course experience. + +### Routing + +Routes are defined in [src/constants.ts](src/constants.ts) as `DECODE_ROUTES` and `ROUTES`. The main route structure is: +- `/course/:courseId/home` — Course outline/home tab +- `/course/:courseId/dates` — Dates tab +- `/course/:courseId/progress` — Progress tab +- `/course/:courseId/discussion/...` — Discussion tab +- `/course/:courseId/:sequenceId/:unitId` — Courseware (unit content) +- `/course/:courseId/course-end` — Course exit + +All routes under `DECODE_ROUTES` are wrapped in `` (URL decoding support). The app entry point is [src/index.jsx](src/index.jsx). + +### Redux Store Structure + +The Redux store ([src/store.ts](src/store.ts)) has these slices: + +| Key | Purpose | +|-----|---------| +| `models` | Normalized model store (courses, sequences, units by ID) | +| `courseware` | Current courseId, sequenceId, unit IDs and loading state | +| `courseHome` | Course home tab data | +| `specialExams` | Special exam state (external lib) | +| `learningAssistant` | AI chat assistant state (external lib) | +| `recommendations` | Course exit recommendations | +| `tours` | Product tour state | +| `plugins` | Plugin framework overrides | + +**Model Store pattern**: API data is normalized and stored flat in `state.models.[id]`. Slices store IDs, not full objects. Access via `state.models.courses[state.courseware.courseId]`. See [docs/decisions/0004-model-store.md](docs/decisions/0004-model-store.md). + +### Key Source Directories + +- [src/courseware/](src/courseware/) — Unit content delivery: sequences, units, iframes, sidebar + - [src/courseware/data/](src/courseware/data/) — Redux slice, thunks, API, selectors for courseware + - [src/courseware/course/sequence/](src/courseware/course/sequence/) — Sequence navigation & unit rendering + - [src/courseware/course/new-sidebar/](src/courseware/course/new-sidebar/) — Sidebar (discussions, notifications) +- [src/course-home/](src/course-home/) — Outline, dates, progress, discussion tabs + - Each tab has its own `data/` subdirectory with slice, thunks, and API +- [src/generic/](src/generic/) — Domain-agnostic reusable code (model-store, hooks, notices, user-messages). Do not add app-specific logic here. +- [src/shared/](src/shared/) — App-specific shared code used across multiple top-level components +- [src/plugin-slots/](src/plugin-slots/) — `@openedx/frontend-plugin-framework` plugin slots for extensibility +- [src/tab-page/](src/tab-page/) — `TabContainer` wrapper that handles tab loading state +- [src/product-tours/](src/product-tours/) — Onboarding product tours + +### Naming Conventions + +From [docs/decisions/0006-thunk-and-api-naming.md](docs/decisions/0006-thunk-and-api-naming.md): + +- **API functions** use HTTP verb prefixes: `getCourseBlocks`, `postSequencePosition` +- **Redux thunks** use semantic prefixes: `fetchCourse`, `fetchSequence`, `saveSequencePosition`, `checkBlockCompletion` + +### Data Flow Pattern + +Each major feature follows this pattern: +``` +data/api.js — Raw API calls (HTTP verb naming) +data/thunks.js — Redux thunks that call APIs and dispatch to model-store (fetch/save naming) +data/slice.js — Redux slice (state shape + reducers) +data/selectors.js — Reselect selectors +data/__factories__/ — Rosie factories for test data +``` + +### Loading State + +Components own their own loading state (LOADING/LOADED/FAILED/DENIED constants from [src/constants.ts](src/constants.ts)). Components render spinners/skeletons themselves rather than relying on parents to gate rendering. See [docs/decisions/0005-components-own-their-loading-state.md](docs/decisions/0005-components-own-their-loading-state.md). + +### Plugin Slots + +The app exposes many plugin slots via `@openedx/frontend-plugin-framework` in [src/plugin-slots/](src/plugin-slots/). Each slot has a README. Slots allow operators to inject/replace UI components without forking. + +### Testing Approach + +From [docs/decisions/0007-testing.md](docs/decisions/0007-testing.md): + +- Use **React Testing Library** — query by labels, text, roles; use `data-testid` as last resort +- Mock HTTP with **axios-mock-adapter**; build test data with **Rosie factories** in `data/__factories__/` +- Test non-obvious behavior (error states, interactions, corner cases) — not happy-path rendering +- **Avoid snapshots** for complex components; they're too brittle. Snapshots are acceptable for data/redux tests and tiny isolated components. \ No newline at end of file From 4daa61301c6bcb241e891b8d9136d96eaba9ddb6 Mon Sep 17 00:00:00 2001 From: nsprenkle Date: Mon, 9 Mar 2026 10:50:20 -0400 Subject: [PATCH 02/11] docs: add note about forks & upgrades --- CLAUDE.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index fd007bc171..883ab40b34 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,6 +29,24 @@ npm run types # tsc --noEmit (TypeScript only) npm run i18n_extract # Extract translation strings ``` +## Fork and Upgrade Strategy + +This repo is a fork of [openedx/frontend-app-learning](https://github.com/openedx/frontend-app-learning) maintained in the `edx` GitHub org. Understanding this relationship is important when authoring changes. + +### Release cadence + +The open source community authors changes on `openedx/master`. Every ~6 months, these are grouped into a named release branch, tested, and offered to forks. Because we run a large production instance, we pull in upstream changes only when named releases are available — not from `openedx/master` directly. + +### Authoring changes + +We also author our own changes on this fork to ship features faster than the upstream review process allows. The guiding principle is: **every change should ideally be compatible with upstream and eventually contributed back.** + +In practice this means: + +- **Prefer plugin slots** — proprietary UI additions should use `@openedx/frontend-plugin-framework` plugin slots ([src/plugin-slots/](src/plugin-slots/)) so they can be injected without modifying core code. +- **Prefer feature toggles disabled by default** — any behavior that isn't appropriate for the broader open source community should be gated behind a config flag (via `getConfig()` from `@edx/frontend-platform`) that defaults to off, making the change safe to contribute upstream. +- **Avoid proprietary logic in core paths** — changes that can't be upstreamed should be isolated at the edges (plugin slots, config-gated branches) rather than embedded in shared data/API/redux code. + ## Architecture This is an OpenEdX Micro-Frontend (MFE) built on `@edx/frontend-platform` and `@openedx/frontend-build`. It serves the learner-facing course experience. From eb904c38de0dd7052f34d21067d8834e52e1c2cf Mon Sep 17 00:00:00 2001 From: nsprenkle Date: Mon, 9 Mar 2026 10:57:02 -0400 Subject: [PATCH 03/11] feat: add validate command to run pre-PR validation --- .claude/commands/validate.md | 46 ++++++++++++++++++++++++++++++++++++ CLAUDE.md | 2 ++ 2 files changed, 48 insertions(+) create mode 100644 .claude/commands/validate.md diff --git a/.claude/commands/validate.md b/.claude/commands/validate.md new file mode 100644 index 0000000000..dc2f8356a3 --- /dev/null +++ b/.claude/commands/validate.md @@ -0,0 +1,46 @@ +Run all pre-PR validation checks that mirror CI, then report results. + +Execute the following checks **in order**, capturing output from each. Continue through all checks even if one fails — collect all failures before reporting. + +## Checks to run + +### 1. Commit messages +Run: `git log main..HEAD --format="%H %s"` + +For each commit, validate the subject line against the conventional commits format: +`(): ` + +Valid types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, `revert` + +Flag any commit whose subject does not match this pattern. + +### 2. Lint +Run: `npm run lint -- --max-warnings 0` + +### 3. Type checking +Run: `npm run types` + +### 4. Tests +Run: `npm test -- --passWithNoTests` + +### 5. Build +Run: `npm run build` + +### 6. Bundle size +Run: `npm run bundlewatch` + +## Report + +After all checks complete, output a summary table: + +| Check | Status | +|-------|--------| +| Commit messages | ✅ PASS / ❌ FAIL | +| Lint | ✅ PASS / ❌ FAIL | +| Types | ✅ PASS / ❌ FAIL | +| Tests | ✅ PASS / ❌ FAIL | +| Build | ✅ PASS / ❌ FAIL | +| Bundle size | ✅ PASS / ❌ FAIL | + +For each failed check, show the specific errors and a brief suggested fix. +If all checks pass, confirm the branch is ready for a PR. diff --git a/CLAUDE.md b/CLAUDE.md index 883ab40b34..32789fa4ee 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,6 +4,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Commands +Run `/validate` to check all pre-PR requirements locally (lint, types, tests, build, bundle size, commit messages) and get a pass/fail summary with guidance on any failures. + ```bash # Development npm start # Start dev server (standard) From c8bd5bdee7feb290e8bc2f29ce24e27cc43281a1 Mon Sep 17 00:00:00 2001 From: nsprenkle Date: Mon, 9 Mar 2026 15:20:40 -0400 Subject: [PATCH 04/11] feat: add validate command and stub config to work regardless of local plugin setup --- .claude/commands/validate.md | 2 +- package.json | 1 + webpack.validate.config.js | 21 +++++++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 webpack.validate.config.js diff --git a/.claude/commands/validate.md b/.claude/commands/validate.md index dc2f8356a3..d9efee8470 100644 --- a/.claude/commands/validate.md +++ b/.claude/commands/validate.md @@ -24,7 +24,7 @@ Run: `npm run types` Run: `npm test -- --passWithNoTests` ### 5. Build -Run: `npm run build` +Run: `npm run build:validate` ### 6. Bundle size Run: `npm run bundlewatch` diff --git a/package.json b/package.json index 49e01aee27..35565940a9 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ ], "scripts": { "build": "fedx-scripts webpack", + "build:validate": "fedx-scripts webpack --config webpack.validate.config.js", "bundlewatch": "bundlewatch", "i18n_extract": "fedx-scripts formatjs extract", "lint": "fedx-scripts eslint --ext .js --ext .jsx --ext .ts --ext .tsx .", diff --git a/webpack.validate.config.js b/webpack.validate.config.js new file mode 100644 index 0000000000..eea55ef2a0 --- /dev/null +++ b/webpack.validate.config.js @@ -0,0 +1,21 @@ +const path = require('path'); +const { merge } = require('webpack-merge'); +const prodConfig = require('@openedx/frontend-build/config/webpack.prod.config'); + +/** + * Webpack config used by `npm run build:validate`. + * + * Identical to the prod build except env.config is replaced with a stub so + * that the build succeeds without the private edX plugin packages that are + * only available in the local development monorepo. + */ +module.exports = merge(prodConfig, { + resolve: { + alias: { + 'env.config': path.resolve(__dirname, './env.config.validate'), + // TsconfigPathsPlugin doesn't hook correctly on the merged config, so + // replicate the tsconfig "@src/*" path mapping explicitly. + '@src': path.resolve(__dirname, 'src'), + }, + }, +}); From 089425fbacbce6b52612f0ec08d48e1f50c8fb71 Mon Sep 17 00:00:00 2001 From: nsprenkle Date: Mon, 9 Mar 2026 15:21:07 -0400 Subject: [PATCH 05/11] chore: add allowed npm run scripts --- .claude/settings.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .claude/settings.json diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000000..a806b4a2f8 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,12 @@ +{ + "permissions": { + "allow": [ + "Bash(npm run lint)", + "Bash(npm run types)", + "Bash(npm test -- --passWithNoTests)", + "Bash(npm run build)", + "Bash(npm run build:validate)", + "Bash(npm run bundlewatch)" + ] + } +} From e773f134277a2100e5cc662ee0191bd0574a845d Mon Sep 17 00:00:00 2001 From: nsprenkle Date: Tue, 10 Mar 2026 15:20:24 -0400 Subject: [PATCH 06/11] chore: add more allowed scripts to settings.json --- .claude/settings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.claude/settings.json b/.claude/settings.json index a806b4a2f8..46563453a4 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -2,6 +2,7 @@ "permissions": { "allow": [ "Bash(npm run lint)", + "Bash(npm run lint -- --max-warnings 0)", "Bash(npm run types)", "Bash(npm test -- --passWithNoTests)", "Bash(npm run build)", From fe4492c83e9a48fdb1b6b8c2c4d53821892a7679 Mon Sep 17 00:00:00 2001 From: Nathan Sprenkle Date: Thu, 12 Mar 2026 14:47:01 -0400 Subject: [PATCH 07/11] feat: add additional missing allow scripts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .claude/settings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.claude/settings.json b/.claude/settings.json index 46563453a4..79731ff38c 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -7,6 +7,7 @@ "Bash(npm test -- --passWithNoTests)", "Bash(npm run build)", "Bash(npm run build:validate)", + "Bash(git log -1 --pretty=%B)", "Bash(npm run bundlewatch)" ] } From 846f621dedd52024557e80e3007ee710c8dd6f1a Mon Sep 17 00:00:00 2001 From: Nathan Sprenkle Date: Thu, 12 Mar 2026 14:48:13 -0400 Subject: [PATCH 08/11] fix: update to correct config location Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- webpack.validate.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack.validate.config.js b/webpack.validate.config.js index eea55ef2a0..eac07def47 100644 --- a/webpack.validate.config.js +++ b/webpack.validate.config.js @@ -1,6 +1,6 @@ const path = require('path'); const { merge } = require('webpack-merge'); -const prodConfig = require('@openedx/frontend-build/config/webpack.prod.config'); +const prodConfig = require('./webpack.prod.config'); /** * Webpack config used by `npm run build:validate`. From 19589a0734452c75dd93a8d1d7b6d08847d42add Mon Sep 17 00:00:00 2001 From: Nathan Sprenkle Date: Thu, 12 Mar 2026 14:48:33 -0400 Subject: [PATCH 09/11] fix: update path Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- webpack.validate.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack.validate.config.js b/webpack.validate.config.js index eac07def47..cf117efadc 100644 --- a/webpack.validate.config.js +++ b/webpack.validate.config.js @@ -12,7 +12,7 @@ const prodConfig = require('./webpack.prod.config'); module.exports = merge(prodConfig, { resolve: { alias: { - 'env.config': path.resolve(__dirname, './env.config.validate'), + 'env.config': path.resolve(__dirname, './env.config.validate.jsx'), // TsconfigPathsPlugin doesn't hook correctly on the merged config, so // replicate the tsconfig "@src/*" path mapping explicitly. '@src': path.resolve(__dirname, 'src'), From 49110e9734d09a1698ac2dd7f2ac506ac3077484 Mon Sep 17 00:00:00 2001 From: nsprenkle Date: Wed, 15 Apr 2026 14:32:05 -0400 Subject: [PATCH 10/11] fix: update validate command to correct release branch name --- .claude/commands/validate.md | 4 +++- .claude/settings.json | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.claude/commands/validate.md b/.claude/commands/validate.md index d9efee8470..d3bd291612 100644 --- a/.claude/commands/validate.md +++ b/.claude/commands/validate.md @@ -5,7 +5,9 @@ Execute the following checks **in order**, capturing output from each. Continue ## Checks to run ### 1. Commit messages -Run: `git log main..HEAD --format="%H %s"` +Run: `git log release-teak..HEAD --format="%H %s"` + +> Note: `release-teak` is the current base branch for PRs in the `edx` fork. Update this (and the matching allow-list entry in `.claude/settings.json`) when the default branch changes. For each commit, validate the subject line against the conventional commits format: `(): ` diff --git a/.claude/settings.json b/.claude/settings.json index 79731ff38c..8c812da26b 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -8,6 +8,7 @@ "Bash(npm run build)", "Bash(npm run build:validate)", "Bash(git log -1 --pretty=%B)", + "Bash(git log release-teak..HEAD --format=\"%H %s\")", "Bash(npm run bundlewatch)" ] } From 1e7d0f85ec1e493f294631f0b3ebaff321c6f5e0 Mon Sep 17 00:00:00 2001 From: nsprenkle Date: Wed, 15 Apr 2026 14:33:01 -0400 Subject: [PATCH 11/11] fix: correct some validate command details --- .claude/commands/validate.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.claude/commands/validate.md b/.claude/commands/validate.md index d3bd291612..2a4e1f0594 100644 --- a/.claude/commands/validate.md +++ b/.claude/commands/validate.md @@ -1,4 +1,6 @@ -Run all pre-PR validation checks that mirror CI, then report results. +Run all pre-PR validation checks and report results. + +> Note: The build step uses `npm run build:validate` (with a stub `env.config`) rather than `npm run build`, so the build succeeds without the private edX plugin packages required in production. All other checks match CI. Execute the following checks **in order**, capturing output from each. Continue through all checks even if one fails — collect all failures before reporting.