We appreciate any community contributions to this project, whether in the form of issues or pull requests.
This document explains how to set up the repository, how packages and reference implementations fit together, which validation to run for common kinds of changes, and which local hooks and CI behaviors to expect.
Many subtrees also contain local AGENTS.md files. They are written for agent tooling, but they
also serve as concise local runbooks for humans. When you begin working in a package,
implementation, or lib/ workspace, read the nearest AGENTS.md for subtree-specific commands and
gotchas.
Working on your first Pull Request? You can learn how from this extensive list of resources for people who are new to contributing to Open Source.
Table of Contents
The following software is required or strongly recommended for day-to-day development:
- Node.js (use
.nvmrcwhen possible; the minimum supported version is specified inpackage.json) - pnpm (the pinned version is recorded in the root
package.json) jqfor the localpre-pushhook- Docker Desktop or any Docker-compatible
container manager when working on
implementations/web-sdk - An Android emulator when running React Native Detox flows in
implementations/react-native-sdk
Note
Browser-based E2E flows also require Playwright browser binaries. The targeted
pnpm setup:e2e:<implementation> wrappers install them for browser implementations.
After cloning the repository:
nvm use
pnpm install
pnpm version:node
pnpm version:pnpmpnpm install also installs the local Husky hooks used during commit and push.
| Path | Purpose |
|---|---|
lib/ |
Internal shared tooling and mock services, such as build-tools and mocks |
packages/ |
Workspace packages, including the published SDKs and framework layers |
implementations/ |
Reference applications used for integration testing, local demos, and E2E coverage |
pkgs/ |
Generated tarballs created by pnpm build:pkgs; implementations install from these |
docs/ |
Generated TypeDoc output |
dist/, coverage/ |
Generated build and test artifacts inside individual workspaces |
The most important repository-specific mechanic is this:
- Package changes do not automatically flow into reference implementations.
- If an implementation consumes a package you changed, rebuild tarballs with
pnpm build:pkgsand reinstall that implementation before trusting local results. - The targeted
pnpm setup:e2e:<implementation>wrappers do this refresh for you as part of E2E setup.
The root package.json contains more scripts than are listed below. These are the
ones most contributors need regularly.
| Command | When to use it |
|---|---|
pnpm lint |
Lint lib/ and packages/ workspaces |
pnpm implementation:lint |
Lint implementations/ |
pnpm implementation:install |
Refresh all implementations after rebuilding package tarballs |
pnpm typecheck |
Type-check all workspaces |
pnpm implementation:typecheck |
Type-check all implementations |
pnpm test:unit |
Run unit tests across workspace packages |
pnpm build |
Build all @contentful/* packages |
pnpm build:pkgs |
Build packages and create implementation-consumable tarballs in pkgs/ |
pnpm size:check |
Validate bundle-size budgets for all built packages |
pnpm size:report |
Report bundle sizes without failing on budgets |
pnpm docs:generate |
Generate TypeDoc output |
pnpm format:check |
Check repository formatting |
pnpm setup:e2e:<implementation> |
Prepare one implementation for E2E, including package refresh and local setup |
pnpm test:e2e:<implementation> |
Run one implementation's full E2E flow |
pnpm implementation:run -- <implementation> ... |
Run a specific helper action or package-local script inside one implementation |
pnpm playwright:install |
Install Playwright browsers across implementations |
pnpm playwright:install-deps |
Install Playwright system dependencies on Linux |
pnpm serve:mocks |
Run the shared mock services used by local flows |
Most workspaces also define targeted local scripts such as dev, build, test:unit, and
size:check. Prefer targeted validation in the affected package or implementation instead of
running the whole repository when the change is narrow.
Before running pnpm implementation:lint across the repository, run pnpm implementation:install
so the reference implementations have current local package tarballs installed.
- Read the nearest package or
lib/AGENTS.md. - Make your change in the package source, tests, docs, or local harness.
- Run targeted validation in that workspace.
- If the package is consumed by an implementation you want to verify, rebuild tarballs and reinstall the implementation before running local integration or E2E checks.
Example:
pnpm lint
pnpm --filter @contentful/optimization-web typecheck
pnpm --filter @contentful/optimization-web test:unit
pnpm --filter @contentful/optimization-web build
pnpm --filter @contentful/optimization-web size:check
pnpm build:pkgs
pnpm implementation:run -- web-sdk implementation:installIf your next step is E2E rather than a narrow manual reinstall, prefer the targeted wrapper:
pnpm setup:e2e:web-sdk
pnpm test:e2e:web-sdk- Read the nearest implementation
AGENTS.md. - If your change depends on freshly built local packages, run
pnpm build:pkgsand reinstall the implementation first. - Run targeted implementation validation.
- Run the implementation's E2E flow for user-visible or integration-heavy changes.
Example:
pnpm implementation:run -- web-sdk_react typecheck
pnpm implementation:run -- web-sdk_react build
pnpm implementation:run -- web-sdk_react implementation:test:e2e:run- Create a local
.envfrom the implementation's.env.exampleif the implementation expects one and you do not already have it. - Run the targeted setup wrapper.
- Run the targeted E2E wrapper.
Example:
cp implementations/web-sdk/.env.example implementations/web-sdk/.env
pnpm setup:e2e:web-sdk
pnpm test:e2e:web-sdkEnvironment notes:
web-sdkrequires Docker because the app is served via nginx.- Browser implementations require Playwright browser binaries.
react-native-sdkrequires an Android emulator for Detox flows.- Several implementations use PM2-managed processes for local serving; stop only the relevant implementation process rather than doing broad PM2 cleanup.
implementation:run is the shared helper used by the implementation scripts listed above.
Run a helper action for all implementations:
pnpm implementation:run -- --all -- <action> [args...]Run a helper action for one implementation:
pnpm implementation:run -- <implementation> <action> [args...]<implementation>is a folder name underimplementations/(for example:web-sdk,web-sdk_react,react-web-sdk,node-sdk,node-sdk+web-sdk,react-native-sdk)<action>can be one of these helper actions:implementation:installimplementation:build:runimplementation:test:unit:runimplementation:playwright:installimplementation:playwright:install-depsimplementation:setup:e2eimplementation:test:e2e:run<action>can also be any implementation-local script name (for example:serve,serve:stop,test:e2e:ui)[args...]are forwarded to the target script/action (when supported by that action/script)
Examples:
# Install dependencies for every implementation
pnpm implementation:run -- --all -- implementation:install
# Run the implementation-level E2E command for all implementations
pnpm implementation:run -- --all -- implementation:test:e2e:run
# Run one implementation's local script
pnpm implementation:run -- web-sdk test:e2e:ui
# Pass arguments through to the underlying E2E script
pnpm implementation:run -- node-sdk implementation:test:e2e:run -- --grep "homepage"Prefer the root wrapper scripts when they already match what you want to do. For example:
pnpm implementation:installpnpm setup:e2e:<implementation>pnpm test:e2e:<implementation>
Use the smallest meaningful validation set for the change.
| Change type | Usually run | Notes |
|---|---|---|
| Docs-only or markdown-only | pnpm format:check |
Also run pnpm docs:generate if public API docs or linked markdown changed |
Package or lib/ TypeScript change |
Targeted lint, typecheck, test:unit, and build |
Prefer pnpm --filter <workspace> ... when the change is narrow |
| Built package runtime, export, dependency, or bundle change | Targeted size:check or root pnpm size:check |
Bundle-size checks are contributor-run validation, not a dedicated CI job |
| Package change used by an implementation | pnpm build:pkgs, then reinstall the affected implementation |
Use pnpm setup:e2e:<implementation> if E2E is your next step |
| Implementation code change | pnpm implementation:lint, targeted implementation typecheck, and implementation-local tests |
Some implementations have no meaningful unit tests; E2E matters more there |
| Shared build or packaging change | Broaden validation to downstream packages and at least one affected implementation | Examples: lib/build-tools, package exports, tarball/install flow |
| User-visible integration or runtime behavior change | Targeted implementation E2E | Choose the implementation that exercises the changed surface |
When in doubt, start targeted and broaden only if the change crosses package or implementation boundaries.
This project uses ESLint and Prettier to enforce coding and formatting conventions. You can enable related editor plugins to get the same feedback while working on Optimization SDKs.
Please review the following files to familiarize yourself with current configurations:
Local Git hooks installed by Husky:
pre-commitrunslint-stagedon staged files.prepare-commit-msgopens a Commitizen prompt when you do not supply a commit message and you are not amendingHEAD.pre-pushruns targeted type checks and unit tests for changed workspaces and implementations.
Practical implications:
- If you change TypeScript in a workspace package, expect targeted typecheck and unit-test work to happen again on push.
- If you change an implementation, expect targeted implementation typecheck and implementation
test:unitwork to run on push. - Fix hook failures rather than bypassing them unless you have an explicit reviewed reason to skip them.
Code is documented using TSDoc, and reference documentation is generated using TypeDoc and published automatically with each new version.
- authored supporting docs belong in
documentation/, whiledocs/remains generated output pnpm docs:generategenerates documentation from TSDoc code comments, package README files, and markdown files underdocumentation/pnpm docs:watchwatches for file updates and rebuilds documentation output; useful while writing and updating documentation
When changing public SDK behavior in this pre-release alpha period, update the same pull request to keep these artifacts aligned:
- TSDoc/JSDoc comments near changed API surfaces
- package READMEs that document those surfaces
- package-local development harnesses or example flows when they are meant to demonstrate the changed behavior
- any replacement design, architecture, or specification artifacts that the repository adds for the changed area
documentation/ contains source markdown that TypeDoc publishes. docs/ is generated output. Do
not hand-edit generated TypeDoc output.
READMEs are orientation surfaces, not the only place for every detail. Match depth to the README category:
- Application-facing package READMEs keep purpose, install, minimal setup, common options, critical caveats, and links to guides, reference implementations, and generated API reference.
- Lower-level package READMEs explain the package's role in the SDK stack, who uses it directly, common setup options where useful, and where exhaustive API details live.
- Reference implementation READMEs stay procedural: what the implementation demonstrates, prerequisites, setup, run/test commands, environment notes, and related package links.
- Internal and placeholder READMEs stay short, explicit, and status-oriented.
Move step-by-step implementation material into the existing guide for that runtime when one exists. Create a new guide only when no existing guide covers the reader goal. Keep exhaustive method catalogs, callback payload shapes, and exported type details in TypeDoc.
Package README links must work in GitHub source browsing, generated TypeDoc project documents, and npmjs README rendering. Use canonical generated-doc URLs for shared header navigation and verify repo-relative links before relying on package README publish rewriting.
- An implementation is not reflecting your package change: run
pnpm build:pkgs, then reinstall the affected implementation withpnpm implementation:run -- <implementation> implementation:install. - Playwright reports a missing browser: run
pnpm playwright:install, or use the targetedpnpm setup:e2e:<implementation>wrapper. - Playwright system dependencies are missing on Linux: run
pnpm playwright:install-deps. implementations/web-sdkfails to serve locally: confirm Docker is running.- React Native Detox cannot attach to a device: confirm an Android emulator is already running.
- An implementation behaves differently from expected local settings: compare its local
.envwith its checked-in.env.example. - A local port such as
3000,8000, or8081is already in use: stop only the relevant local process or implementationserveflow rather than using broad PM2 cleanup.
Main Pipeline runs implementation E2E jobs when path filters request them for both:
- pull requests
- pushes to
main
This is an intentional CI policy:
- E2E execution is path-filtered to reduce CI runtime and cost.
- CI E2E runs against local mock services via checked-in
.env.examplevalues to keep runs deterministic and stable. - Production/live-server E2E is a manual verification step when needed; it is intentionally not part of default CI.
The path filters do not watch only implementation directories. Shared package and root changes can also trigger implementation E2E. At a high level:
| Workflow filter -> job(s) | Also watches shared surfaces |
|---|---|
e2e_node_sdk -> e2e-node-sdk |
lib/**, packages/node/node-sdk/**, universal packages, root package/workflow files |
e2e_node_sdk_web_sdk -> e2e-node-sdk-web-sdk |
lib/**, packages/node/node-sdk/**, packages/web/web-sdk/**, packages/web/preview-panel/**, shared root files |
e2e_web_sdk -> e2e-web-sdk |
lib/**, packages/web/web-sdk/**, packages/web/preview-panel/**, universal packages, shared root files |
e2e_web_sdk_react -> e2e-web-sdk_react |
lib/**, packages/web/web-sdk/**, packages/web/preview-panel/**, universal packages, shared root files |
e2e_react_web_sdk -> e2e-react-web-sdk |
lib/**, packages/web/frameworks/react-web-sdk/**, packages/web/web-sdk/**, packages/web/preview-panel/**, universal packages, shared root files |
e2e_react_native_android -> e2e-react-native-android-build, e2e-react-native-android |
lib/**, packages/react-native-sdk/**, universal packages, shared root files |
See .github/workflows/main-pipeline.yaml for the exact
authoritative filter list.
Skipping an implementation E2E job because its filter did not match is expected behavior, not a CI coverage defect.
If a change needs to trigger an implementation E2E job but does not match the current filters,
update the path filters in .github/workflows/main-pipeline.yaml in the same pull request.
E2E setup does not depend on repository secrets. Each implementation creates .env from its own
checked-in .env.example file in CI, which keeps fork PR behavior aligned with internal PRs.
Run license-checker locally:
pnpx license-checker --summary
pnpx license-checker > licenses.txtIf the license for a package merely has a spelling or formatting difference from an existing entry
in the license-check GitHub workflow allow list, update the list and submit the change via pull
request. Otherwise, create an issue to receive further guidance from the maintainers.