Blockscout Frontend

repository·main·Indexed 43 days ago

https://github.com/blockscout/frontend

The user interface for the Blockscout blockchain explorer. Distributed as a Docker image and configured via environment variables, it includes a three-stage build process and a runtime orchestration sequence for assets, favicons, and sitemaps. Also provides the @blockscout/ui-toolkit, a pre-configured Chakra UI theme and component library for the Blockscout design system.

Tokens
68.4K
Snippets
191
Records
313
Agent score
55%

What's inside Blockscout

  1. Understand the Blockscout frontend container build and runtime lifecycle

    main

    The Blockscout frontend container is built using a three-stage Dockerfile (deps → builder → runner) and is initialized via entrypoint.sh.

    Build Time

    During the build phase, orchestration scripts in deploy/scripts/ prepare the application:

    • build_sprite.sh: Assembles SVG icons from src/sprite/icons/ into public/icons/sprite.<hash>.svg. The hash is exported as NEXT_PUBLIC_ICON_SPRITE_HASH for cache-busting.
    • collect_envs.sh: Scans docs/ENVS.md for NEXT_PUBLIC_* variables and creates .env.registry. This registry acts as a contract; only variables listed here are exposed to the browser. It also captures build arguments (Git tag, commit SHA, etc.) into .env.
    • Next.js Build: Runs pnpm routes:generate and pnpm run build to produce the standalone server bundle.

    Runtime

    When the container starts, entrypoint.sh executes a sequence of steps to configure the environment before handing control to the Next.js server (node server.js). These steps include loading presets, downloading external assets, validating environment variables, generating favicons/OG images, and generating configuration files (sitemaps, LLMS text, etc.).

  2. Understand the public directory structure

    main

    The /public directory contains several subdirectories used for different types of assets during build and runtime:

    • /icons: Contains SVG-sprite assets. These are generated at build time.
    • /static: Contains static assets that remain consistent between application re-runs, though they may change between different build versions.
    • /assets: Contains dynamically generated assets created when the application starts. This includes the favicon bundle, the environment variables (ENV) file, and external application configurations.
  3. Use the Pagination API components and hooks

    main

    The pagination system is composed of several specialized files. When building or consuming paginated lists, refer to these core pieces:

    • types.ts: Defines PaginationParams, the object returned by hooks and consumed by controls.
    • useApiPaginatedQuery.ts: The primary composition hook. It combines usePaginationParams and usePaginationActions with useApiQuery to provide the full object list components consume.
    • usePaginationParams.ts: Derives page, cursor, filters, and sorting from the URL.
    • usePaginationActions.ts: Provides handlers for next, prev, first, filter, and sorting. Each handler performs a single router.push.
    • Pagination.tsx & StickyPaginationWithText.tsx: The UI controls for navigating pages.
    • useDebouncedFilterChange.ts: Handles debouncing search-box input to URL updates.
    • utils.ts: Provides generateListStub (for placeholderData) and emptyPagination (for local or non-paginated lists).
  4. Understand the Cognitive Complexity (CC) scoring model

    main

    The project uses a custom Cognitive Complexity (CC) model for code quality gates that diverges from the standard SonarSource model. The core philosophy is to "forgive breadth, punish nesting." This model is designed to distinguish between code that is long/flat (high breadth) and code that is deeply nested (high complexity).

    Key Scoring Rules

    1. Quadratic Nesting Penalty: Unlike the linear model (which uses 1 + n), this model uses 1 + n² (where n is the 0-based depth). Every structural increment that carries a nesting penalty—such as if, ternary operators, loops, catch, and switch—pays this quadratic price.

      • else and else if increments stay flat at +1.
      • switch statements count as a single increment regardless of the number of cases.
      • The penalty is identical to the linear model at depths 0 and 1, but diverges significantly from the third level onwards.
    2. Null-Coalescing Operators: The null-coalescing operators ?? and ??= are ignored and add 0 to the complexity score. They are treated as readable shorthand (similar to optional chaining ?.) rather than control flow paths.

    3. Boolean Operators: Standard boolean operators && and || still cost +1 per run.

    Important Warnings

    • Do not expect parity with SonarSource: If you compare these scores against eslint-plugin-sonarjs, the Blockscout scores will be higher for any code nested past depth 1. This is intentional design, not a bug.
    • Coupling with Configuration: The complexity caps defined in ../config.ts (e.g., DEFAULT_MAX_COGNITIVE_BEHAVIOR) are calibrated specifically to this quadratic model. Changing the scoring logic without recalibrating the caps will make the quality gates ineffective.
  5. Understand the Code Complexity Model

    main

    The code complexity tool uses two primary metrics to gate code quality: Cognitive Complexity (CC) for readability and CRAP (Change Risk Anti-Patterns) for risk assessment.

    Both scores are computed via a single walk of the syntax tree using the TypeScript compiler API. The model distinguishes between two function classes which are gated differently:

    • jsx: Functions where JSX appears directly in the body (e.g., components or inline .map() render callbacks). These have a high cognitive complexity backstop (--max-cognitive-jsx) but are never scored for CRAP because their rendering is covered by Playwright visual tests rather than Vitest coverage.
    • behavior: Everything else (event handlers, hooks, utils, useCallback/useMemo bodies). These have a tighter cognitive cap (--max-cognitive-behavior) and are scored for CRAP.

    Key Principle: The model's thesis is that breadth is cheap, depth is not.

  6. Conventions for writing envs-validator rules

    main

    When defining Yup schemas for environment variables, adhere to these specific patterns to ensure correct validation and helpful error messages:

    URL Validation

    Do not use yup.string().url(). The built-in validator is disabled. Instead, use the custom urlTest from utils.ts:

    NEXT_PUBLIC_SOMETHING_URL: yup.string().test(urlTest),

    JSON-shaped values

    Variables containing JSON objects or arrays must use .transform(replaceQuotes).json() before defining the shape. This allows operators to use single quotes in .env files without escaping:

    yup.object<MyType>().transform(replaceQuotes).json().shape({ … })

    Companion variables (Conditional logic)

    When variable B depends on variable A, use .when() with a guard.

    • For scalars: Assert the value is undefined in the otherwise block.
    • For arrays/strings: Use .max(-1) in the otherwise block to ensure even empty values fail if the condition isn't met.

    Deeply-nested JSON

    To avoid unhelpful default Yup error messages for complex nested objects, wrap the rule in yup.mixed().test('shape', ...) and perform the validation inside the test function using isValidSync.

    // Example of Deeply-nested JSON convention
    NEXT_PUBLIC_FOO: yup.mixed().test(
      'shape',
      'Invalid schema for NEXT_PUBLIC_FOO, it should have …',
      (data) => {
        const isUndefined = data === undefined;
        const valueSchema = yup.object<Foo>().transform(replaceQuotes).json().shape({
          name: yup.string().required(),
          url_template: yup.string().required(),
        });
        return isUndefined || valueSchema.isValidSync(data);
      },
    ),
  7. Understand the Code Complexity tool architecture

    main

    The tool is organized into four pipeline stages that separate measurement from judgment. The caps (thresholds) are only applied at the final analysis stage.

    Pipeline Stages

    • Selection (select/): Determines which files and line ranges are in scope (e.g., using git diffs).
    • Measurement (measure/): Performs AST walks to compute raw numbers without applying caps. Includes:
      • complexity.ts: Calculates cognitive and cyclomatic complexity.
      • jsx.ts: Detects JSX to determine coverage requirements.
      • crap.ts: Implements the CRAP formula.
    • Coverage (coverage/): Provides the input for the CRAP score by running Vitest and parsing coverage-final.json to derive per-function coverage.
    • Rendering (render/): Outputs the results via a score table or GitHub annotations.

    Core Components (Root)

    • index.ts: The CLI entrypoint that orchestrates the pipeline.
    • config.ts: Defines the three complexity caps used in both local and CI runs.
    • analyze.ts: The single point where complexity and coverage are joined to produce a verdict (pass/fail) for each function.
    • run.sh: A compile-on-run wrapper.
  8. Understand environment variable layering and precedence

    main

    The dev server uses dotenv-cli to load multiple environment files. Precedence is determined by the order of files: the FIRST file listed has the highest priority.

    Layering for dev:preset and prod:preset (Highest to Lowest):

    1. --port <number> (via dotenv-cli -v)
    2. .env.local (Personal, git-ignored overrides)
    3. .env.extra (Committed branch/feature ENVs; no secrets)
    4. .env.secrets (Local secret overrides; git-ignored)
    5. .env.tmp (Fetched instance configuration)

    Layering for dev:local (Highest to Lowest):

    1. --port <number>
    2. .env.local
    3. .env.extra
    4. .env.secrets
    5. .env.localhost (Committed base config for local backends)
  9. Understand mutation testing outcomes

    main

    The mutation testing process produces several distinct outcomes:

    • SURVIVED: A failure. A code change was made, but no test assertion caught it. You must add an assertion, delete the code, or suppress the mutant.
    • NO COVERAGE: Not a failure of the mutation gate. This means no test reached the line at all. This is handled by the CRAP (Code Responsibility for Astro Programming) gate, which reports insufficient coverage.
    • Run TRUNCATED: The wall-clock budget expired before the run could complete. A truncated run is not a pass; a green result from a truncated run is invalid. See ./docs/RUNNING.md for details.
  10. Understand the App Config Convention

    main

    The Blockscout frontend uses a centralized configuration pattern where src/config/index.ts acts as an aggregator for various co-located config modules. This ensures that raw environment variables are transformed into typed, validated data in a single, predictable way.

    To maintain architectural integrity, the project enforces three strict rules via ESLint:

    1. Single Default Export: A config module must not export anything except its default object. All feature-specific configuration must reside within this object. Types and constant lists should be moved to sibling files like types/config.ts or consts.ts rather than being exported from the config module.
    2. Centralized Imports: Application code must only import from src/config. You should access configuration via the pattern config.<section>.<name>. Never import individual config modules directly into feature code.
    3. Isolated Environment Access: Only config modules are permitted to read environment variables. The utility src/config/utils/envs.ts and the raw window.__envs map should only be accessed within the src/config directory. Reading environment variables elsewhere leads to untyped, unvalidated, and potentially inconsistent data between Node and the browser.
  11. Compare Oxlint and Biome for linting replacement

    main

    When considering a replacement for ESLint in this repository, two main candidates were evaluated:

    Oxlint

    • Strategy: Recommended as a hybrid layer (running alongside ESLint).
    • Pros: Supports a migration path via @oxlint/migrate; can run core and TS rules extremely fast; additive approach means no loss of custom rules.
    • Cons: Full replacement depends on the stability of its JS-plugin runtime (currently in alpha).

    Biome

    • Strategy: Formatter-first model.
    • Pros: High performance.
    • Cons: Its model conflicts with the current @stylistic-defined house style; its plugin model cannot host the custom rules required by this project.
  12. Where resource response types originate

    main

    Response types for API resources are sourced from two primary locations:

    1. Generated npm packages: Most services use types generated from OpenAPI specs in their respective API source repositories. These are published as @blockscout/* packages. The frontend pins specific versions of these packages in package.json.
    2. Locally-defined types: Certain types are defined within the frontend repository itself (e.g., in src/**/types/api.ts).

    When types are kept local by design:

    • Whole APIs: Some APIs like metadata:*, userOps, or clusters (external tRPC) are typed locally.
    • Core API gaps: Some core:* resources lack OpenAPI schemas and are hand-typed.
    • Filter & Sorting: Parameters like TokensFilters or TokenTransferFilters are shaped locally in src/slices/**/types/api.ts for better usability.
    • Socket Messages: WebSocket message types cannot be derived from OpenAPI.
    • Convenience Aliases: Deeply nested types are often aliased locally (e.g., AddressCeloAccount).
    • Feature-owned Proxied Data: When the Core API proxies data from a micro-service that the Core API's own schema doesn't fully describe, the precise shape is owned by the feature. The feature defines the type in src/features/**/types/api.ts, and the frontend slice may consolidate these by replacing generic generated union members with these specific feature types.
    import type { schemas } from '@blockscout/api-types';
    import type { SearchResultTacOperation } from 'src/features/chain-variants/tac/types/api';
    import type { SearchResultDomain }       from 'src/features/name-services/domains/types/api';
    
    export type SearchResultItem = 
      Exclude<schemas['SearchResultItem'], { type: 'tac_operation' | 'ens_domain' }> | 
      SearchResultTacOperation | 
      SearchResultDomain;