Skip to content
Merged
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 thread
nsprenkle marked this conversation as resolved.
/**
* 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