Skip to content

Latest commit

 

History

History
170 lines (127 loc) · 23.6 KB

AGENTS.md

File metadata and controls

170 lines (127 loc) · 23.6 KB

Repository conventions

Generated release files

  • CHANGELOG.md is generated by the release pipeline during publication. Do not edit it manually; document behavior and migration guidance in the relevant documentation instead.

Production code changes during test work

  • Authorization to add, migrate, refactor, or accelerate tests does not authorize changes to production code, including CLI, build configuration, resolvers, plugins, public contracts, or resource lifecycles. Do not change working production behavior merely to accommodate a test runner, fixture layout, parallel execution, or an experimental test backend.
  • Distinguish a product defect from a limitation of the test setup. Before proposing a production fix, demonstrate how supported consumer usage violates the intended contract, independently of test-only assumptions. A failing experiment or a faster test run alone is not evidence of a product defect. Keep a confirmed fix within an already authorized scope; otherwise present the evidence and obtain agreement before changing production code.
  • First use the existing public boundaries and realistic execution model in tests. Prefer properly isolated fixtures, processes, and resources owned by the test session. Preserve scenario coverage and observable behavior; do not hide failures with private implementation access, hidden global state, arbitrary retries, or special production branches for tests.
  • If a sound test-layer solution is unavailable, stop the affected implementation and propose alternatives before editing production code. Explain the ownership boundary, consumer impact, lifecycle and cleanup, concurrency and platform constraints, tradeoffs, and validation. Choose established mechanisms that fit this codebase; neither novelty nor passing tests justifies an architectural workaround.
  • Treat newly discovered production changes as a separate scope decision, even inside an approved test optimization pilot. Agreement on a product fix does not authorize unrelated infrastructure redesign. Keep product fixes and test-only changes separately reviewable.

Cross-platform tests

  • Regular CI in .github/workflows/ci.yml runs on Linux and Windows with Node.js 24. Pull requests targeting main and pre-release validation use the full Node.js 22/24 matrix. New and changed non-browser tests must work on both operating systems and supported Node.js versions. Chrome and Firefox browser integrations run on Linux for each selected Node.js version; consult the workflow for the current matrix. Coverage and package publication use Node.js 24.
  • Build filesystem paths with path.resolve and path.join, including paths used with in-memory filesystems. Do not hardcode Unix roots such as /dist or Windows drive letters. Configure the compiler and read, write, or assert its output using the same resolved directory. Keep filesystem paths distinct from URLs and manifest asset paths, which use forward slashes.
  • In watch tests, simulate edits with writeFile(destination, await readFile(source)). Do not use copyFile to update watched files: on Windows it can preserve an old mtime, preventing the watcher from detecting the change. Copying initial fixtures before starting watch is fine.
  • Wait for the expected compilation or observable output with a descriptive timeout. Do not rely on fixed sleeps or assume that emitted files mean the watcher has finished reconnecting. Preserve exact filename casing and account for platform line endings when they are not part of the tested contract.
  • A local pass on one operating system does not establish cross-platform compatibility. For platform-specific failures, reproduce the relevant filesystem or process behavior when possible and distinguish that simulation from validation on the actual CI runner. Do not hide the failure by skipping the platform or only increasing timeouts.

Entrypoint parsing tests

  • src/cli/entrypoint/file reads source files without knowing entrypoints: a parser only supplies a definition name and its schema keys through AbstractParser.optionFile(). Organize its tests and fixtures by what the reader can read, not by entrypoint: export shapes in tests/fixtures/export-shapes, static value resolution in static-options, and expression kinds in expression. Cover every way a file may declare options there once; do not repeat that matrix per entrypoint.
  • src/cli/entrypoint/parser tests are organized by entrypoint: one <Name>Parser.test.ts per parser, or one kebab-case file using describe.each for a family whose parsers differ only in the definition they recognise. Keep fixtures in tests/fixtures/<entrypoint>/options/<scenario>/, invalid/, and contracts/. Use real entrypoint files rather than mocked readers or generated source strings.
  • A parser test proves what the parser owns: its definition is recognised and a sibling's is ignored; a full scenario reads every accepted key through its own definition; defaults; invalid values of its own keys; entrypoint-specific interpretation; and contract() when the parser declares an agreement. The full scenario is also how an entrypoint proves that it adopted a shared schema fragment.
  • Test a shared schema fragment once at its owner: the common filters in AbstractParser.test.ts; view and CSP options belong beside ViewParser and ViewCspParser; a mixin schema beside its module in parser/schemas. Do not add a cross-cutting test file with one row per adopting entrypoint.
  • A schema fragment that unrelated parsers opt into, such as PermissionsSchema, is a mixin: keep it as a module in src/cli/entrypoint/parser/schemas and merge it explicitly in each adopting parser. Do not put it on AbstractParser, which holds only what every entrypoint shares, and do not add an intermediate parser class for it: independent mixins cannot be combined through single inheritance.
  • Do not re-test export shapes such as named exports, default objects, as, or satisfies in a parser test. Add a parser-level shape scenario only when the entrypoint gives that shape a meaning, for example the Content default render or the Relay init.

Responsibility boundaries

Layer ownership

  • src/main is the public API surface. It exposes functions and re-exports contracts; it must not become the internal source of shared types or reusable implementation helpers.
  • src/types owns shared domain contracts, enums, and constants that genuinely belong to those contracts. Keep these modules free of implementation dependencies and initialization side effects. Internal code imports shared types from @typing/*, not from @main/*.
  • src/shared owns pure implementation helpers shared by CLI and extension runtime, grouped by domain (for example, shared/content). Such helpers may use domain contracts, but must not import CLI/runtime implementations or depend on DOM state. Keep them separate from public exports and declaration-only contracts.
  • Generated, augmentable contracts follow the same rule: for example, PageAliasRegistry, PageAlias, and PageMap belong in types/page.ts and are re-exported through main/page.ts. Preserve public module augmentation and verify both public and internal consumers when moving them.
  • src/cli/entrypoint owns discovery, parsing, naming, and normalized entrypoint descriptions. Reuse its finders instead of reaching into a feature plugin for the same information.
  • src/cli/entrypoint/parser owns entrypoint schemas, defaults, and interpretation of parsed data. These classes, including AbstractParser, configure file readers but must not traverse TypeScript AST or construct Program/TypeChecker instances. Keep source analysis in file and its specialized file/parsers; return plain metadata so each entrypoint decides what an export means.
  • src/cli/plugins owns framework feature policy and composition: interpreting entrypoint options, configuring the bundler, supplying normalized data, and declaring manifest requirements.
  • src/cli/bundler/plugins owns Rspack integration: compiler hooks, runtime modules, emitted assets, and generic build validation. A class using Rspack hooks belongs here even when only one framework feature currently uses it.
  • src/entry and the other extension runtime modules own execution, rendering adapters, and lifecycle behavior. Build code must not import runtime implementations to obtain constants, types, or helpers.
  • Public functions intentionally used as APIs, such as definePlugin, are not forbidden imports. Distinguish calling a public API from making the public facade the owner of a shared internal contract. If implementation logic needs multiple internal consumers, extract it to the appropriate shared layer and keep its public export.

Dependencies between plugins

  • A framework feature plugin must not import or instantiate another feature plugin's manager or implementation. For example, content uses PageFinder from @cli/entrypoint, not Page from plugins/page, to resolve isolation.page aliases.
  • Framework plugins configure Rspack plugins by passing data, selectors, or callbacks with explicit contracts. Do not pass a manager instance, including a disguised dependency such as Pick<ContentManager, ...>.
  • Give Rspack plugins names and options that describe their build responsibility, not their current caller. They should not know about content options such as isolation.page, page aliases, or specific managers when they only need filenames and access requirements.
  • Caller-specific policy and diagnostic context belong to the framework plugin. Pass a validation callback, issuer, or hint instead of hardcoding the caller's options and error explanation inside a generic Rspack plugin.
  • Rspack plugins must not import sibling plugin implementations or their private helpers. Extract a genuinely shared contract or algorithm below both consumers; do not hide the dependency behind a barrel re-export.
  • Use bundler/plugins/utils and bundler/plugins/types.ts for shared plugin infrastructure such as compilation hooks and option contracts. Use bundler/utils for general asset graph, classification, and filename utilities. Keep feature-private helpers inside their owning plugin.
  • Prefer an existing shared abstraction over a new service, registry, or global context introduced only to avoid one import. Importing the correct shared finder is sufficient when its data is what the consumer needs.

Shared contracts and runtime delivery

  • Separate a shared protocol from its implementation. A runtime property name used by both CLI and extension runtime belongs in a side-effect-free shared contract; each side imports it independently. The generic Rspack plugin receives the property through its options, using the shared property contract.
  • Do not move build-only details into public domain types merely because a feature uses them. For example, IsolatedStylesLayer is a bundler classification detail; a property used to communicate with the extension's styles runtime is a shared contract.
  • Keep each Rspack plugin encapsulated in its own directory with its class, tests, helpers, templates, and an index.ts exporting the necessary surface. Runtime JavaScript templates belong beside their owning plugin and use the existing raw-template pipeline; do not mix unrelated plugin runtimes in a common directory or embed large JavaScript strings in plugin classes.
  • DefinePlugin is appropriate for compile-time constants and substitutions. Data depending on final asset names needs the bundler's filename and hashing lifecycle, not an early snapshot or an ad hoc post-build rewrite.
  • Refactoring must not introduce background dependencies into other entrypoints, hidden global registries, message-based metadata delivery, or extra carrier files. Such behavior requires a separate, explicit design decision.

Asset inventory, delivery, and access

  • The entrypoint asset map describes which files belong to an entrypoint. Keep js and css complete; do not add feature-specific delivery subsets such as isolatedCss to this public inventory.
  • Delivery belongs to the relevant runtime/plugin. For example, the isolated styles runtime owns its initial CSS list and loading state; Shadow/iframe nodes register their render roots without reading or filtering the public asset map.
  • Manifest contracts represent manifest data and generic dependencies, not feature configuration. Do not add routing fields such as isolatedStyles, isolatedCss, or framePage to transport policy through the manifest builder. Use the appropriate builder methods and shared preparation/validation hooks.
  • Resource registration and access validation are separate responsibilities. The page plugin declares WAR from page.matches; the consumer supplies its required resource and origins; the generic validator checks the final merged manifest. Validation must not silently grant or broaden access.
  • Perform checks that depend on emitted files or user manifest overrides at the appropriate final compilation stage. Refresh derived data per compilation in watch mode so removed entries, aliases, styles, and WAR rules do not survive through stale state.

Refactoring checks

  • For experimental APIs that have not been released and are still being designed in the active branch, replace superseded contracts outright. Do not retain compatibility aliases, migration guards, obsolete-contract tests or migration documentation unless explicitly requested. Keep validation and tests for the currently agreed contract.
  • Before adding an import or moving a symbol, identify who owns the behavior, who needs the data, and which dependency direction follows from that ownership. A directory move alone does not fix a dependency if the original implementation is still imported through a helper or re-export.
  • When changing a shared boundary, update declarations, consumers, templates, tests, and documentation together. Treat removal of public metadata as an API change, not a cosmetic cleanup.
  • Preserve behavior while separating responsibilities: public exports, generated declarations, loading order, lifecycle, user filename templates, manifest/WAR semantics, and watch updates. Verify at the owning layer and use existing integration tests for cross-layer guarantees.
  • Apply these rules to new and changed code. Report unrelated existing violations rather than expanding a focused task into an unrequested architectural rewrite.

Built-in storage

  • Every built-in storage uses a namespace exactly matching its owning package name, including case. In this framework, use PackageName from @typing/app (adnbn) rather than repeating a literal.
  • Features define default keys inside that shared namespace; locale selection defaults to locale. Keep the namespace inside the built-in storage implementation; callers may configure the key through its constructor. Custom sources implement the feature's storage driver contract.

Internal imports

  • Import internal TypeScript modules through configured aliases or relative paths without a file extension. Do not write the future .js output path into a TypeScript source import.
  • Output extensions and external-module paths belong to the build configuration. When a bundled entry needs an external dependency, resolve its source import to the emitted ESM path during the build.
  • Imports of actual JavaScript files, package subpaths, and loader-specific resources such as .ts?raw keep the syntax required by their owner. This rule does not rewrite raw template contents or generated JavaScript imports.
  • A directory of functions inside an entrypoint runtime, such as src/entry/<name>/resolvers, has no index.ts. Import the owning file, for example ./resolvers/definition, so the path shows where the logic lives. Directories of classes follow the class export rules below.
  • An entrypoint runtime's index.ts is its package subpath adnbn/entry/<name>. List its exports explicitly and do not use export *, so an internal helper never becomes public by accident.
  • Apply this convention to new and changed imports; do not mass-rewrite unrelated files.

Code layout

  • Always write if and else bodies as multiline blocks with braces, even for a single return, throw, or expression. Never use inline guards such as if (!current()) return;.
  • Put a blank line before return when another statement precedes it in the same block. A return that is the first or only statement in a block follows the opening brace directly, as in the example below.
  • Separate a multiline statement or code block from adjacent code with a blank line above and below. This includes multiline declarations, calls, callbacks, and control-flow blocks. Do not add empty padding immediately inside opening or closing braces, and keep } else {, } catch (...) {, and } finally { together.
  • Apply these rules to implementation code, tests, fixtures, and examples. Keep formatting changes within the files being worked on unless a broader cleanup is explicitly requested.
if (!current()) {
    return;
}

const node = createNode(anchor);

if (!node.target) {
    node.unmount();

    return;
}

register(node);

return node;

Symbol naming

  • Apply these conventions throughout the project: shared types, runtime APIs, entrypoints, CLI, bundler plugins, and tests.
  • Before introducing or renaming a symbol, inspect related declarations and reuse their domain vocabulary and capitalization, for example Entrypoint, not EntryPoint.
  • Keep third-party contracts and platform API names unchanged; these rules apply to symbols owned by the project.
  • Use PascalCase for classes, interfaces, type aliases, enums, and enum members. Use camelCase for functions, methods, parameters, and local variables.
  • Name related exported types using domain + concept + role or variant. Keep the shared family prefix intact and append the distinguishing part: ContentScriptContainerOptions, ContentScriptContainerFactory, ViewRenderHandler, EntrypointAssetsMapEntry.
  • Give each family a clear base name for its main contract. Name related shapes consistently, for example EntrypointAssets, EntrypointAssetsFiles, EntrypointAssetsMap, and EntrypointAssetsMapEntry.
  • Suffixes must describe the actual role or shape: Map for a keyed collection, MapEntry for its value, Options for configuration, and Handler for a callable handler. Do not use the same name for different data shapes.
  • Name classes after their responsibility and role, such as LocaleFinder or ContentParser. Avoid names tied only to a temporary implementation detail or former file location.
  • Keep private helpers and local variables concise when their surrounding scope already supplies the domain. Do not mechanically repeat a long public prefix everywhere.

Type declarations and reuse

  • Apply these conventions to every type the project owns: shared contracts, runtime APIs, entrypoints, CLI, bundler plugins, and tests.
  • Refer to a type by its name wherever it appears in class and function code: return types, parameters, properties, generic arguments, and local annotations. Do not reach into another type with an indexed access such as ReadonlyConfig["homepage"] or ViewItem<O>["options"]. The reader has to open the other declaration to learn what the member is, and the signature silently changes when that declaration does.
  • Reuse before declaring. Look for the named type that already describes the value, starting in src/types, and use it instead of describing the same shape again inline. For example, use ManifestHostPermissions rather than Set<string>, EntrypointFile rather than {file: string; import: string}, and ViewItems<O> rather than Map<string, ViewItem<O>>. One concept has one name across types, parsers, finders, plugins, and builders.
  • When no name exists, declare one where the concept is owned: a shared contract in src/types, a type private to one module beside its only consumer. Follow the symbol naming rules above, and do not keep two names for the same shape.
  • For a third-party type that exposes only a nested member, add a local alias in the owning contract, as types/manifest.ts does for chrome.runtime.ManifestPermission, instead of indexing it inside a class.
  • Build a contract by composing named contracts rather than copying their fields, for example PopupConfig & PermissionsOptions & CspOptions & ViewOptions. A contract that only part of the entrypoints adopt stays a separate named mixin.
  • Indexed access remains appropriate inside type-level utilities that are generic over the key, such as PluginHandlerOptions<K>, where no single named type can replace it.

Class exports

  • When a class is the main export of a file, always export it by default. Keep the class explicitly named to match the file, for example export default class ReactLocale in ReactLocale.ts.
  • When a directory's index.ts exposes that class, use a named re-export of its default: export {default as ReactLocale} from "./ReactLocale";. Consumers import the class by name from that index.

Public API naming

  • Use verb + domain concept for exported functions. Prefer explicit names over ambiguous shortcuts, and use the same terminology as the associated types.
  • For getters returning a named domain contract, align the function and result names: getX(): X. Preserve required wrappers such as Promise<X>. A getter returning a collection should also express that collection in its name.
  • For example, use getEntrypointAssets(): EntrypointAssets and getEntrypointAssetsMap(): EntrypointAssetsMap.
  • Add qualifiers such as Current, Runtime, or Build only when they identify a meaningful distinction. Do not add them merely to resolve a naming collision; first clarify the base contract and its related shapes.
  • Keep access restrictions and behavior explicit in the API documentation and errors. A naming cleanup must not change the returned data, execution context, or loading behavior.

Renaming existing symbols

  • Update declarations, imports, re-exports, callers, tests, fixtures, and examples together, including the root addon playground and relevant documentation.
  • Treat exported renames as public API changes. Do not silently preserve old aliases or remove compatibility that has been explicitly required.
  • Keep renames focused. Do not change behavior or sweep unrelated existing names into the same change. Check for stale references and run validation appropriate to the affected code.

File and directory naming

  • Use PascalCase for a file whose primary export is a class or React component, matching that export's name: LocaleFinder.ts, BuildAssetsMapPlugin.ts, and DiagnosticPanel.tsx. Prefer a named export for function components; primary classes follow the default-export rule above.
  • Name tests for a specific class after that class, preserving PascalCase and adding the test suffix: BuildAssetsMapPlugin.test.ts, ChunkLoaderPlugin.test.ts, and ContentManager.test.ts.
  • Use lowercase kebab-case for other multiword filenames, including helper modules, types, enums, configuration modules, and tests that are not dedicated to a class: file-precedence.ts, entrypoint-assets.ts, and entrypoint-assets.integration.test.ts.
  • Use lowercase kebab-case for multiword directory names, including directories containing classes or React components: entrypoint-assets and diagnostic-panel.
  • Preserve tool-required conventional names such as AGENTS.md and README.md, and recognized suffixes such as .content.ts, .service.ts, .module.css, and .config.ts. This rule does not change TypeScript symbol casing.
  • Apply the rule to new and renamed paths. Do not mass-rename unrelated existing files as part of another task.
  • Keep an entrypoint or component with its own styles and resources in one directory. A standalone entrypoint without related files can remain a single file.
  • Keep finder-specific helper modules in src/cli/entrypoint/finder/utils.