Skip to content
50 changes: 50 additions & 0 deletions .claude/commands/validate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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.

## Checks to run

### 1. Commit messages
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:
`<type>(<optional scope>): <description>`

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:validate`

### 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.
15 changes: 15 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"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)",
"Bash(npm run build:validate)",
Comment thread
nsprenkle marked this conversation as resolved.
"Bash(git log -1 --pretty=%B)",
Comment thread
nsprenkle marked this conversation as resolved.
"Bash(git log release-teak..HEAD --format=\"%H %s\")",
"Bash(npm run bundlewatch)"
]
}
}
132 changes: 132 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# CLAUDE.md

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

## 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)
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
```

## 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.

### 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 `<DecodePageRoute>` (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.<type>[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.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 .",
Expand Down
21 changes: 21 additions & 0 deletions webpack.validate.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const path = require('path');
const { merge } = require('webpack-merge');
const prodConfig = require('./webpack.prod.config');

Comment on lines +2 to +4
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This config claims to be identical to the prod build, but it merges @openedx/frontend-build/config/webpack.prod.config instead of the repo’s ./webpack.prod.config.js (which adds customizations like copying public/static and the @src alias). Consider importing and extending the local webpack.prod.config.js so build:validate matches the actual production build output/behavior apart from the env.config stub.

Copilot uses AI. Check for mistakes.
/**
* 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.jsx'),
// TsconfigPathsPlugin doesn't hook correctly on the merged config, so
// replicate the tsconfig "@src/*" path mapping explicitly.
'@src': path.resolve(__dirname, 'src'),
},
},
});
Loading