diff --git a/.claude/commands/validate.md b/.claude/commands/validate.md new file mode 100644 index 0000000000..2a4e1f0594 --- /dev/null +++ b/.claude/commands/validate.md @@ -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: +`(): ` + +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. diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000000..8c812da26b --- /dev/null +++ b/.claude/settings.json @@ -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)", + "Bash(git log -1 --pretty=%B)", + "Bash(git log release-teak..HEAD --format=\"%H %s\")", + "Bash(npm run bundlewatch)" + ] + } +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..32789fa4ee --- /dev/null +++ b/CLAUDE.md @@ -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 `` (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 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..cf117efadc --- /dev/null +++ b/webpack.validate.config.js @@ -0,0 +1,21 @@ +const path = require('path'); +const { merge } = require('webpack-merge'); +const prodConfig = require('./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.jsx'), + // TsconfigPathsPlugin doesn't hook correctly on the merged config, so + // replicate the tsconfig "@src/*" path mapping explicitly. + '@src': path.resolve(__dirname, 'src'), + }, + }, +});