Thanks for taking the time to contribute. This guide covers everything you need to get a working local environment, run the test suite, and get your PR reviewed.
- Node.js 20.x (see
.nvmrcor usenvm use 20) - npm 10+ (bundled with Node 20)
- PostgreSQL 16 (or use the provided Docker Compose setup)
- Git
Optional but recommended:
- Docker + Docker Compose (simplifies database setup)
- k6 for load tests
git clone https://github.com/Ethereal-Future/FuTuRe.git
cd FuTuRe
npm installcd backend
cp .env.example .envOpen backend/.env and fill in the required values. At minimum you need:
DATABASE_URL— PostgreSQL connection stringJWT_SECRET— any strong random string for local devSTREAM_SECRET_ENCRYPTION_KEY— 32-byte hex key (see comment in.env.example)
See backend/CONFIGURATION.md for the full reference.
Using Docker (recommended):
# from the repo root
docker compose up db -dOr point DATABASE_URL at an existing local PostgreSQL 16 instance.
cd backend
npx prisma migrate deployFrom the repo root:
npm run devThis starts both servers concurrently:
| Service | URL |
|---|---|
| Backend | http://localhost:3001 |
| Frontend | http://localhost:3000 |
The backend uses --watch for hot-reload. The frontend uses Vite HMR.
npm run test:coveragenpm run test --workspace=backendRequires a running PostgreSQL instance (use docker compose up db -d):
npm run test:db --workspace=backendnpm run test:contractsnpm run test:propertyRequires k6 and a running backend:
npm run load-test:endpoints --workspace=backend
npm run load-test:concurrent --workspace=backend
npm run load-test:regression --workspace=backendThis section is the complete reference for writing, running, and reasoning about tests in this project. A new contributor should be able to follow it from top to bottom without prior knowledge of the test setup.
Day-one task: Before you write any code, run all three test suites locally and confirm they pass. This validates your environment and gives you a baseline to compare against.
| Suite | Command | When to use |
|---|---|---|
| Unit + integration (single run) | npm run test:coverage |
Before every PR |
| Unit + integration (watch mode) | npm run test:watch |
During development |
| Backend tests only | npm run test --workspace=backend |
When changing backend code |
| DB integration tests | npm run test:db --workspace=backend |
When changing Prisma schema or queries |
| E2E tests (headless) | npx playwright test |
Before every PR, from e2e/ directory |
| E2E tests (headed — visual) | npx playwright test --headed |
Debugging a failing E2E test |
| E2E tests (one browser) | npx playwright test --project=chromium |
Faster local iteration |
| Mutation tests | npm run test:mutation |
When adding new unit tests or utility functions |
| Property-based tests | npm run test:property |
Included in npm run test:coverage |
| Contract tests | npm run test:contracts |
When changing API request/response shapes |
The project uses three distinct testing frameworks, each with a specific role.
Vitest covers all unit and integration tests across the frontend and backend. Use Vitest for:
- Pure functions (validators, formatters, utilities)
- Service layer logic
- React component rendering and interactions (
@testing-library/react) - Backend route handlers with a mocked or in-process Express app
- Database integration tests against a real PostgreSQL instance
Do not use Vitest for anything that requires a real browser (use Playwright instead).
Configuration: vitest.config.js (root), vitest.mutation.config.js (mutation testing), vitest.property.config.js (property tests), vitest.contracts.config.js (contract tests).
Coverage thresholds (enforced in CI):
| Metric | Threshold |
|---|---|
| Lines | 80% |
| Functions | 80% |
| Branches | 75% |
| Statements | 80% |
Playwright drives a real browser to test complete user flows. Use Playwright for:
- Multi-step user journeys (registration → onboarding → dashboard)
- Anything that depends on browser APIs (cookies,
localStorage, clipboard) - Cross-browser compatibility (Chromium, Firefox, WebKit, mobile viewports)
- Visual regression (
@visualtag)
Do not write Playwright tests for logic that Vitest can cover. A Playwright test is 10–50× slower than a Vitest test.
Configuration: e2e/playwright.config.js.
Stryker measures test quality by introducing small deliberate bugs (mutations) into the source code and checking whether the test suite catches them. A high line-coverage figure can hide weak assertions — Stryker makes the quality of assertions visible.
Use the mutation score as a signal, not a hard constraint:
| Score | Meaning |
|---|---|
| ≥ 80 (high) | Acceptable — assertions are meaningful |
| 60–79 (low) | Flag for improvement before merging |
| < 50 (break) | CI fails — too many mutations survive |
Mutation testing targets frontend/src/utils/*.js and backend/src/services/*.js. It does not run on every PR by default (it is slow); it runs in CI on the test:mutation:ci pipeline job.
nvm use 20
node --version # v20.x.x
npm --version # 10.x.xnpm install # from the repo rootdocker compose up db -dThis is required for DB integration tests (test:db) and for any test that uses Prisma against a real database.
cd backend
cp .env.example .envFor tests, the minimum required variables are:
DATABASE_URL=postgresql://user:password@localhost:5432/future_remittance
JWT_SECRET=any-string-is-fine-for-testing
STREAM_SECRET_ENCRYPTION_KEY=<32-byte hex — see comment in .env.example>A .env.test file is already present in backend/ with safe test defaults. Vitest picks it up automatically when running npm run test.
cd backend
npx prisma migrate deployThe Playwright binaries are not bundled with npm install. Install them once:
cd e2e
npm install # installs Playwright itself
npx playwright install # downloads browser binaries (~300 MB)
npx playwright install-deps # installs OS-level dependencies (Linux only)Expected output from npx playwright install:
Downloading Chromium 123.0.6312.86 ...
Chromium 123.0.6312.86 downloaded to ~/.cache/ms-playwright/...
Downloading Firefox 124.0 ...
...
# macOS
brew install k6
# Linux
sudo apt-get install k6
# Windows
choco install k6| Code being tested | Test file location | Example |
|---|---|---|
backend/src/auth/tokens.js |
backend/tests/auth.tokens.test.js |
✔ existing |
frontend/src/utils/validateAmount.js |
frontend/src/utils/validateAmount.test.js |
✔ existing |
frontend/src/components/Button.jsx |
frontend/src/components/Button.test.jsx |
✔ existing |
Test files must end in .test.js, .test.jsx, .spec.js, or .spec.jsx.
- One
describeblock per module or function under test. - One
it/testper behaviour, not per line of source code. - Test description reads as a sentence:
it('returns null when the amount is zero'). - Arrange–Act–Assert structure: set up data, call the function, assert the result.
- Use
beforeEachfor per-test setup. Avoid shared mutable state across tests. - Prefer real values over mocks unless the dependency is a network call, database, or clock.
import { describe, it, expect, beforeEach } from 'vitest';
import { signAccessToken, verifyToken, signRefreshToken } from '../src/auth/tokens.js';
import { resetConfig } from '../src/config/env.js';
describe('JWT Tokens', () => {
// ── Setup ──────────────────────────────────────────────────────────────────
// Reset the config singleton and set a deterministic JWT_SECRET before each
// test so tests do not depend on each other's environment state.
beforeEach(() => {
resetConfig();
process.env.JWT_SECRET = 'test-secret-key-12345';
});
// ── Happy path ─────────────────────────────────────────────────────────────
it('signs and verifies an access token', () => {
const payload = { userId: 'user-123', role: 'admin' };
// Act — produce a token from a known payload
const token = signAccessToken(payload);
// Assert — the token is a non-empty string
expect(token).toBeDefined();
// Act — verify the token and unpack the payload
const verified = verifyToken(token);
// Assert — the original claims are present
expect(verified.userId).toBe('user-123');
expect(verified.role).toBe('admin');
});
it('signs and verifies a refresh token', () => {
const payload = { userId: 'user-456' };
const token = signRefreshToken(payload);
const verified = verifyToken(token);
expect(verified.userId).toBe('user-456');
});
// ── Error path ─────────────────────────────────────────────────────────────
// Test the unhappy path explicitly. This verifies that the error is thrown
// rather than silently returning undefined or a bad token.
it('throws when JWT_SECRET is not configured', () => {
resetConfig();
delete process.env.JWT_SECRET;
expect(() => {
signAccessToken({ userId: 'user' });
}).toThrow('JWT_SECRET is not configured');
});
});Key things to notice:
beforeEachresets global state — tests that mutateprocess.envor a singleton must clean up.- Happy path and error path are both tested.
- Each
ithas a single, focused assertion goal. Multi-assertion tests are fine when all assertions test the same behaviour. - No mocks were needed here because
signAccessTokenis a pure function given a stableJWT_SECRET.
Shared test helpers live in testing/ at the repo root and in backend/tests/helpers/. Import from there rather than duplicating setup logic:
import { createTestUser, createTestSession } from '../helpers/factories.js';The Playwright config starts npm run dev automatically when you run tests (webServer in e2e/playwright.config.js). If a server is already running on port 3000, Playwright will reuse it.
# run all e2e tests (from repo root)
cd e2e
npx playwright test
# run headed — opens a browser window you can watch
npx playwright test --headed
# run a single file
npx playwright test tests/onboarding.spec.js
# run a single test by title
npx playwright test --grep "shows public key"
# run on one browser only (faster during development)
npx playwright test --project=chromiumExpected clean-run output:
Running 8 tests using 1 worker
✓ Registration › shows confirmation or redirects to keypair setup (1.3s)
✓ Registration › shows error when email is already registered (2.1s)
...
8 passed (12s)
All E2E tests live in e2e/tests/. Name files after the user flow they cover: onboarding.spec.js, payment.spec.js, settings.spec.js.
For any page visited by more than one test, extract selectors and actions into a page object class. Store page objects in e2e/pages/:
// e2e/pages/LoginPage.js
export class LoginPage {
constructor(page) {
this.page = page;
this.emailInput = page.locator('[data-testid="email"]');
this.passwordInput = page.locator('[data-testid="password"]');
this.submitButton = page.locator('[data-testid="signup-btn"]');
this.errorMessage = page.locator('[data-testid="signup-error"]');
}
async goto() {
await this.page.goto('/signup');
}
async fillAndSubmit(email, password) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
}Then use it in a test:
import { LoginPage } from '../pages/LoginPage.js';
test('shows error for duplicate email', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.fillAndSubmit('alice@example.com', 'Str0ngPass!');
// ...
});Use data-testid attributes for selectors — never CSS classes or element tags, which change with styling refactors. The existing tests in e2e/tests/onboarding.spec.js follow this pattern.
To avoid re-authenticating in every test, use Playwright's storageState to save a logged-in session:
// e2e/auth.setup.js
import { test as setup } from '@playwright/test';
setup('authenticate', async ({ page }) => {
await page.goto('/signup');
await page.fill('[data-testid="email"]', 'test@example.com');
await page.fill('[data-testid="password"]', 'Str0ngPass!');
await page.click('[data-testid="signup-btn"]');
await page.waitForURL(/\/dashboard/);
await page.context().storageState({ path: 'e2e/.auth/user.json' });
});import { test, expect } from '@playwright/test';
// Helper to generate a unique email per test run, preventing conflicts
// when tests run in parallel or are retried.
function uniqueEmail() {
return `test+${Date.now()}@example.com`;
}
const STRONG_PASSWORD = 'Str0ngPass!word';
test.beforeEach(async ({ page, context }) => {
// Start each test with a clean browser state
await context.clearCookies();
await page.evaluate(() => localStorage.clear());
// Intercept Friendbot calls so tests do not depend on the live Stellar testnet.
// The route mock returns a fixed successful response, making tests fast and deterministic.
await page.route('**/friendbot**', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ successful: true, hash: 'b'.repeat(64) }),
});
});
});
test('shows confirmation or redirects to keypair setup after registration', async ({ page }) => {
const email = uniqueEmail();
// Navigate to the signup page
await page.goto('/signup');
await page.fill('[data-testid="email"]', email);
await page.fill('[data-testid="password"]', STRONG_PASSWORD);
await page.click('[data-testid="signup-btn"]');
// Assert — either the email-verification message or keypair setup is visible.
// Both are valid post-registration states depending on server configuration.
await expect(
page.locator('[data-testid="verify-message"], [data-testid="keypair-setup"]'),
).toBeVisible({ timeout: 10000 });
});Key things to notice:
beforeEachclears all browser state — tests are fully isolated.- Network calls to external services are mocked with
page.route(...)— tests are deterministic. - Selectors use
data-testid— resilient to HTML restructuring. - The timeout on
toBeVisibleaccounts for async navigation without relying onwaitForTimeout.
In CI (process.env.CI is set):
retries: 2— a flaky test must fail 3 times in a row before CI marks it as failed.workers: 1— tests run sequentially to avoid port conflicts on shared runners.forbidOnly: true—test.onlyaccidentally left in code causes CI to fail immediately.- Reports are written to
e2e/test-reports/(HTML, JSON, JUnit).
On a local machine, tests run in parallel across available CPUs and retries are disabled.
# Run mutation testing (from the repo root)
npm run test:mutationThis takes several minutes. Stryker targets frontend/src/utils/*.js and backend/src/services/*.js.
Reports are written to mutation-reports/:
mutation-reports/mutation-report.html— open in a browser for the full interactive reportmutation-reports/mutation-report.json— machine-readable output for CI tracking
Stryker prints a summary to the terminal:
Mutation testing [done] 2 minutes
Ran 312 tests across 48 mutants in 105 seconds.
All files
File | % score | # killed | # survived | # no coverage | # error |
-------------------------------|---------|----------|------------|---------------|---------|
validateAmount.js | 91.67 | 11 | 1 | 0 | 0 |
formatBalance.js | 75.00 | 9 | 3 | 0 | 0 |
- killed — the mutation was caught by at least one test. ✔
- survived — the mutation was NOT caught. Your tests did not assert on this behaviour. Add a test that would fail if this code were wrong.
- no coverage — no test exercised the mutated line at all. Add a test for this code path.
| Threshold | Score | Consequence |
|---|---|---|
high |
80 | Target — acceptable quality |
low |
60 | Warning — reviewer should request more tests |
break |
50 | CI hard-fails — PR cannot merge |
When a mutation survives, look at the mutant in the HTML report. Stryker shows you exactly what code it changed and which tests ran. Add an assertion that would catch the described change:
// Surviving mutant: changed `>` to `>=` in validateAmount.js
// Add a boundary test:
it('rejects amount of exactly 0', () => {
expect(validateAmount(0)).toBe(false);
});| Event | Jobs that run |
|---|---|
| Every PR | test (Vitest + coverage), lint, tsc --noEmit, npm audit --audit-level=high, e2e-tests (Playwright) |
Merge to main |
All PR jobs + security-pipeline, docker-scan, performance |
| Weekly schedule | mutation-testing, backup-verification, dependency-updates |
- Click the failing job in GitHub Actions to see the full log.
- For Vitest failures: the log shows the test name, the expected vs received values, and a stack trace. Run the same test locally:
npx vitest run --reporter=verbose <file>. - For Playwright failures: screenshots and videos of the failing test are uploaded as workflow artifacts. Download them from the Actions run summary.
- For mutation testing failures: the
mutation-report.htmlartifact shows exactly which mutants survived. Add tests for the surviving mutants. - For
npm auditfailures: runnpm auditlocally and follow the Dependency Vulnerability Management section.
The backend connects to the Stellar testnet by default. To run against it:
- Set these values in
backend/.env:
STELLAR_NETWORK=testnet
HORIZON_URL=https://horizon-testnet.stellar.org- Start the backend:
npm run dev:backend- Create a test account via the frontend or the API — new accounts are automatically funded by Friendbot.
Never use real Stellar mainnet keys in development. The testnet is reset periodically; any balances will be lost.
-
Fork the repo and create a branch from
main:git checkout -b feat/your-feature-name
-
Make your changes. Keep commits focused — one logical change per commit.
-
Ensure all checks pass locally before pushing:
npm run test:coverage npm audit --audit-level=high
-
Push your branch and open a pull request against
main. -
Fill in the PR template. Include:
- What the change does and why
- How you tested it
- Any follow-up work or known limitations
-
A maintainer will review within a few business days. Address feedback by pushing new commits — do not force-push after review has started.
-
Once approved, a maintainer will squash-merge your PR.
- Tests added or updated for new behaviour
-
npm run test:coveragepasses - No new high/critical vulnerabilities (
npm audit --audit-level=high) - Code formatted with
npm run format - PR description explains the change clearly
- If this PR adds, removes, or significantly changes a component, service, or data flow: update
docs/architecture.mdin the same PR
If you are new to this project, complete these tasks before writing any code:
- Install Node.js 20 and verify with
node --version - Clone the repo and run
npm install - Start PostgreSQL with
docker compose up db -d - Copy
backend/.env.exampletobackend/.envand fill in required values - Run
npm run test:coverageand confirm all tests pass - If contributing to frontend: run
cd e2e && npx playwright installto download browser binaries - Run the dev server with
npm run devand confirm both frontend and backend start - Read the Testing section in this file
Use one of these prefixes followed by a short, kebab-cased description:
| Prefix | Use for |
|---|---|
feat/ |
New features |
fix/ |
Bug fixes |
docs/ |
Documentation-only changes |
chore/ |
Dependency bumps, tooling, config |
refactor/ |
Code restructuring without behaviour change |
test/ |
Adding or fixing tests |
Examples:
feat/gdpr-data-export
fix/refresh-token-expiry
docs/security-guide
The project uses ESLint and Prettier. Run the formatter and linter before pushing:
npm run format # applies Prettier
npm run lint # ESLint checkKey conventions:
- ES modules (
import/export) throughout — norequire(). - Async/await preferred over
.then()chains. - No unused variables;
_prefix for intentionally unused parameters. - Keep functions small and single-purpose; avoid deeply nested callbacks.
- All new CSS must use logical properties instead of physical directional ones, so layouts mirror correctly for RTL locales (Arabic, Hebrew). Use
margin-inline-start/margin-inline-endinstead ofmargin-left/margin-right,padding-inline-start/padding-inline-endinstead ofpadding-left/padding-right,text-align: start/endinstead ofleft/right,border-inline-start/border-inline-endinstead ofborder-left/border-right, andinset-inline-start/inset-inline-endinstead of positioning withleft/right.
New raster assets (PNG/JPG/WebP) must be provided at 1x, 2x, and 3x
resolution, named name.png, name@2x.png, name@3x.png, so they render
sharp on high-DPI (Retina and equivalent) displays instead of the browser
having to upscale the 1x file.
- In JSX, use
<ResponsiveImg src="/logo.png" alt="..." />(frontend/src/components/ResponsiveImg.jsx) instead of a plain<img>— it derives thesrcsetdensity descriptors from the 1x path automatically. - In CSS
background-imagedeclarations, build the value withbuildImageSet()fromfrontend/src/utils/responsiveImage.js. - Prefer SVG for logos/icons where possible — it's resolution-independent and sidesteps this requirement entirely.
Follow the Conventional Commits specification:
<type>(<scope>): <short summary>
[optional body — explain *why*, not *what*]
[optional footer — e.g. Closes #123]
Types: feat, fix, docs, chore, refactor, test, perf.
Examples:
feat(auth): add GDPR data-export endpoint
Implements Article 15 right-of-access requirement.
Closes #503
fix(compliance): filter MEDIUM alerts from SAR reports
Issues labelled good first issue are well-scoped, self-contained tasks with clear acceptance criteria — ideal if you are new to the codebase.
To find them: go to Issues and filter by the good first issue label.
Before starting:
- Comment on the issue to let others know you are working on it.
- Ask any clarifying questions in the issue thread before writing code.
- Keep the PR focused on the acceptance criteria — avoid unrelated refactors.
All uses: references in .github/workflows/ must be pinned to a full commit SHA rather than a mutable version tag.
Why: A tag like actions/checkout@v4 can be silently moved to a different commit by the action author (intentionally or after a supply-chain compromise). If that commit contains malicious code it will execute with access to repository secrets. A pinned SHA is immutable — the exact code that was reviewed is the exact code that runs.
- Find the commit SHA for the version you want:
# Using gh CLI gh api repos/<owner>/<action>/git/refs/tags/<tag> --jq '.object.sha' # If the tag points to an annotated tag object, resolve it: gh api repos/<owner>/<action>/git/tags/<sha> --jq '.object.sha'
- Use the SHA in the workflow file with a human-readable comment:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- Dependabot (configured in
.github/dependabot.yml) will open PRs automatically when a new version is available, updating the SHA to the latest commit for the tag. Review the tag comment to confirm the version before merging.
- Check the action's CHANGELOG / release notes for the new version.
- Confirm the SHA in the PR matches the tag it claims (spot-check via
gh api). - Never approve a SHA update without verifying the tag it corresponds to.
npm audit --audit-level=high runs as a blocking CI step in both test.yml and security-pipeline.yml (covering the root workspace, backend/, and frontend/). A PR cannot merge if any high or critical vulnerability is present in the dependency tree.
Dependabot is configured (.github/dependabot.yml) to open weekly PRs for outdated packages across all three npm contexts and for GitHub Actions. These PRs are labelled dependencies and follow the normal review process.
- Run
npm auditlocally to read the full advisory:npm audit cd backend && npm audit cd frontend && npm audit
- Check the advisory severity, affected versions, and whether a patched version exists.
- If a fix is available, update:
npm audit fix # safe semver-compatible fixes npm audit fix --force # major-version bumps (review breaking changes first)
- If no upstream fix exists yet, assess exploitability in context. If the vulnerable code path is not reachable (e.g., a dev-only package never executed in production), document the exception in a comment on the advisory issue and set a reminder to re-evaluate in 30 days.
- Create a branch:
chore/fix-<package>-vuln. - Update the dependency and run the full test suite:
npm run test:coverage npm audit --audit-level=high
- Open a PR with the advisory ID in the description (e.g.,
Fixes GHSA-xxxx-xxxx-xxxx). - Request review from at least one maintainer — security patches are treated as priority reviews.
- Merge as soon as approved; do not batch security fixes with unrelated changes.
- Check the changelog / release notes for breaking changes before approving.
- Run
npm run test:coverageagainst the branch locally if the package is a critical runtime dependency. - If the update introduces a breaking change that cannot be resolved immediately, close the PR with a comment explaining the blocker and open a tracking issue.
Renovate Bot is configured via renovate.json in the repository root. It automatically opens
PRs for outdated npm dependencies on a weekly schedule (every weekend) and immediately for
security advisories.
| Package set | Behaviour |
|---|---|
| Minor + patch npm updates | Grouped into a single PR per week |
| Security-flagged updates | Separate PR opened immediately, labelled security |
| Major npm updates | Separate PR per package, requires manual review |
| GitHub Actions | Weekly SHA-pin update PR |
| Prisma (client + CLI + adapter) | Grouped together to keep versions in sync |
@stellar/stellar-sdk |
Pinned exact version — update manually with care |
| Lock-file maintenance | Monthly PR to refresh package-lock.json |
- Read the changelog / release notes linked in the PR body.
- Check the CI status — all jobs must pass before merging.
- For
@stellar/stellar-sdkmajor bumps, test against the Stellar testnet before merging. - Merge Renovate PRs promptly — letting them accumulate defeats the purpose of automation.
Both tools are active. Renovate handles richer grouping and scheduling; Dependabot remains as a fallback for GitHub Actions SHA pinning if Renovate is unavailable. If both open a PR for the same package, close the Dependabot one in favour of the Renovate PR.
Every pull request that touches backend/ or frontend/ source or Dockerfiles triggers a
Trivy image vulnerability scan (.github/workflows/docker-scan.yml). The scan also runs
nightly on main to catch newly published CVEs against already-merged images.
The scan fails the pipeline on any HIGH (CVSS ≥ 7.0) or CRITICAL (CVSS ≥ 9.0) CVE that has a known fix available. Unfixed CVEs are reported but do not block the pipeline.
If a CVE is not exploitable in this deployment context (e.g. a vulnerable code path in a library is never reached, or the CVE applies only to a configuration we don't use):
-
Open an issue documenting why the CVE is not exploitable and get approval from a maintainer.
-
Add the CVE ID to
.trivyignorewith a dated comment and a review date ≤ 90 days:CVE-YYYY-NNNNN # Reason: <explanation>. Review by: YYYY-MM-DD. -
Re-evaluate the suppression on the review date. If a fix is now available, remove the suppression and update the base image.
Base image tags (node:20-alpine, nginx:alpine) should be updated regularly. Renovate will
open PRs for Dockerfile base image updates when the SHA-pinned digest becomes outdated.
The backend is being incrementally migrated to TypeScript. The migration plan is documented
in docs/typescript-migration.md.
backend/tsconfig.jsonis configured withallowJs: trueandnoEmit: true.tsc --noEmitruns in CI as part of thelintjob — all.tsfiles must compile cleanly.- Migrated files live at their original path with a
.tsextension (e.g.stellar.ts). - The original
.jsfile is retained until all callers are updated, then removed.
- New files should be written in TypeScript (
.ts), not JavaScript. - Run
npx tsc --noEmitlocally before pushing to confirm the file compiles. - Avoid
@ts-ignore— fix the type. If a third-party type is wrong, use a narrow cast with a comment.
- Copy the JS file to a
.tsfile of the same name. - Add type annotations to all function parameters and return types.
- Fix any
stricterrors — no implicitany. - Open a PR with the label
typescript-migration. - The PR must pass
tsc --noEmitand all existing tests without modification.