Skip to content

Latest commit

 

History

History
109 lines (75 loc) · 8.24 KB

File metadata and controls

109 lines (75 loc) · 8.24 KB

Coding standards

Reference for writing code and tests in this repo. Rules are grouped by concern; apply every rule in the section that matches your task.

Project layout and imports

  • The codebase is TypeScript, ESM. Each utility lives in packages/<package-name> with src for source and tests for tests, split into tests/unit and tests/e2e.
  • Not every workspace is a published package: examples/snippets, layers, and packages/testing share dependencies and tooling with the rest of the monorepo but never ship to npm.
  • Import across packages by package name (import { myFunction } from '@aws-lambda-powertools/commons'), with the dependency declared in the importing package's package.json. Relative paths stay within a package and always carry the .js extension (from './utils.js').
  • Utilities and types shared by two or more packages belong in @aws-lambda-powertools/commons.
  • Sibling-package dependencies (including peerDependencies) are exact pins matching the current lockstep version ("@aws-lambda-powertools/commons": "2.35.0"), no range specifiers.

Package anatomy

Conventions inside each package:

  • Errors live in a per-package errors.ts; classes carry the Error suffix (IdempotencyKeyError) and extend the package's base error.
  • Constants live in a per-package constants.ts as as const objects, in place of TS enums.
  • Environment variables are read through the commons helpers — getStringFromEnv, getNumberFromEnv, getBooleanFromEnv from @aws-lambda-powertools/commons/utils/env — rather than process.env.
  • AWS SDK clients get the Powertools user agent at construction: addUserAgentMiddleware(client, '<feature>') from commons.
  • New public entry points (packages build dual ESM/CJS) require entries in both the exports map and typesVersions in the package's package.json.

TypeScript style

Match the existing style of the surrounding code. House conventions:

  • const by default; let only for reassignment.
  • async/await for asynchronous code.
  • for...of for array iteration; Object.entries / Object.keys / Object.values for object iteration.
  • Specific types first, unknown when the type is genuinely unknown, any only when unavoidable.
  • Fix type errors at their source; a cast (as Type, and especially as unknown as Type) is a last resort for genuine boundaries, paired with a comment explaining why it's safe.
  • import type / export type for type-only imports and exports.
  • null means the absence of a value; undefined means a value not yet set or initialized. Pick the one that matches the meaning.
  • Descriptive, spelled-out names; readability over cleverness.
  • To suppress a type error, use @ts-expect-error with a reason (@ts-ignore is a lint error).
  • Biome organizes imports on save/commit; leave import ordering to it.

JSDoc

Document functions, classes, and types with a single JSDoc block per symbol — public APIs always, internal ones too.

  • Open with one active-voice sentence ending in a period (Gets the user by ID.), then longer detail as needed.
  • @param name - the name of the user: active voice, no type annotation. For option objects: @param {Object} options then @param {string} options.name - the name of the user.
  • @example to show usage.
  • Link symbols as {@link functionName | functionName} — backticks mandatory for docs rendering.
  • @internal marks symbols outside the public API; @deprecated always carries a reason and an alternative.
  • Skip @returns and @throws.

Verification

Run from the repo root with -w <workspace>, or from the package directory:

  • npm run lint to check; npm run lint:fix to auto-fix (review its changes).
  • npm run build:tests to type-check source and tests without emitting — CI compiles them separately from running them. npm run build additionally compiles the CommonJS target.

Unit tests

Tests use vitest and live in each package's tests/unit directory. Run with npm run test:unit -w packages/<name> (or npm run test:unit from the package directory). Write unit tests only — end-to-end tests happen when the user asks for them.

Package test scripts use vitest --run tests/unit for unit tests, vitest --run tests/types --typecheck for type tests, and vitest --run tests/e2e for end-to-end tests. Use echo 'Not Implemented' when a package does not provide a suite.

Coverage: CI enforces 100% coverage on src/** (types files excluded) via npm run test:unit:coverage — the plain test run skips coverage, so verify with the :coverage variant before finishing. Every new source line needs a covering test.

Structure:

  • One behavior per test case. Use it.each to cover input variations of the same behavior.
  • Nest describe blocks at most one level deep.
  • Name tests in active voice, no conditional: it('throws an error when input is invalid').
  • Delimit each test into phases with comments: // Prepare (setup), // Act (execute), // Assess (verify).
  • Commit every test enabled: it.only and .skip are lint errors.

Assertions:

  • Verify results with expect assertions on observable behavior.
  • Each assertion tests something meaningful; test behavior, and reach private methods only by extending the class in the test to expose them.
  • Custom matchers from packages/testing/src/setupEnv.ts: toHaveLogged, toHaveLoggedNth, toHaveEmittedEMFWith, toHaveEmittedNthEMFWith, toHaveEmittedMetricWith, toHaveEmittedNthMetricWith, plus toReceiveCommandWith for mocked AWS SDK clients.

Environment:

  • Keep test cases isolated from each other.
  • vi.mock sparingly, for external dependencies only.
  • console is pre-mocked: use it freely in code under test and in assertions.
  • Set env vars with vi.stubEnv() and restore with vi.unstubAllEnvs() in beforeEach/afterEach; setupEnv pre-sets the standard Lambda env vars.

Invocation-scoped state (tests/unit/concurrency/): when AWS_LAMBDA_MAX_CONCURRENCY is set, Logger, Metrics, and Batch keep per-invocation state in the InvokeStore from @aws/lambda-invoke-store. Otherwise they keep one value shared by all invocations. Two things about that package matter for tests:

  • The stores read globalThis.awslambda.InvokeStore. It only exists after something calls InvokeStore.getInstanceAsync(). The Lambda runtime does that at startup; tests have to do it themselves. Until then, with the env var set, every invocation-scoped read or write throws InvokeStore is not available.
  • The instance is created once and cached. Its kind depends on the env at that moment: with AWS_LAMBDA_MAX_CONCURRENCY set it uses AsyncLocalStorage and isolates invocations; without it, run() gives no isolation. Later calls return the cached instance whatever the env.

So:

  • Call InvokeStore._testing?.reset() in beforeEach to drop the cached instance. setupEnv sets AWS_LAMBDA_BENCHMARK_MODE=1 to expose _testing.

  • Use sequence() from @aws-lambda-powertools/testing-utils to interleave two invocations. It calls getInstanceAsync() for you, so code inside the invocation callbacks needs nothing more.

  • Code that runs before sequence(), such as a constructor or a test of the shared fallback outside any invocation, needs await InvokeStore.getInstanceAsync() after the env stub:

    vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10');
    await InvokeStore.getInstanceAsync();
    const processor = new BatchProcessor(EventType.SQS);
  • Never call getInstanceAsync() before the env stub, for example from a beforeEach that runs ahead of a per-test vi.stubEnv(). It caches the non-isolating store, and tests fail on assertions because one invocation reads the other's state. Nothing throws.

  • Tests for the InvokeStore is not available error stub the global away with vi.stubGlobal('awslambda', undefined). Restore it with vi.unstubAllGlobals() in afterEach or later tests lose it too.

When unsure, copy the pattern of an existing test in the same package.

Documentation

Feature work updates the MkDocs site in docs/features/<utility>.md. Code examples are real TypeScript files in examples/snippets/<utility>/, included via --8<-- snippet syntax — examples/snippets is a workspace that CI lints and type-checks, so every doc example must compile.