CHANGELOG.mdis generated by the release pipeline during publication. Do not edit it manually; document behavior and migration guidance in the relevant documentation instead.
- 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.
- Regular CI in
.github/workflows/ci.ymlruns on Linux and Windows with Node.js 24. Pull requests targetingmainand 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.resolveandpath.join, including paths used with in-memory filesystems. Do not hardcode Unix roots such as/distor 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 usecopyFileto update watched files: on Windows it can preserve an oldmtime, 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.
src/cli/entrypoint/filereads source files without knowing entrypoints: a parser only supplies a definition name and its schema keys throughAbstractParser.optionFile(). Organize its tests and fixtures by what the reader can read, not by entrypoint: export shapes intests/fixtures/export-shapes, static value resolution instatic-options, and expression kinds inexpression. Cover every way a file may declare options there once; do not repeat that matrix per entrypoint.src/cli/entrypoint/parsertests are organized by entrypoint: one<Name>Parser.test.tsper parser, or one kebab-case file usingdescribe.eachfor a family whose parsers differ only in the definition they recognise. Keep fixtures intests/fixtures/<entrypoint>/options/<scenario>/,invalid/, andcontracts/. 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
fullscenario reads every accepted key through its own definition; defaults; invalid values of its own keys; entrypoint-specific interpretation; andcontract()when the parser declares an agreement. Thefullscenario 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 besideViewParserandViewCspParser; a mixin schema beside its module inparser/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 insrc/cli/entrypoint/parser/schemasand merge it explicitly in each adopting parser. Do not put it onAbstractParser, 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, orsatisfiesin 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.
src/mainis 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/typesowns 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/sharedowns 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, andPageMapbelong intypes/page.tsand are re-exported throughmain/page.ts. Preserve public module augmentation and verify both public and internal consumers when moving them. src/cli/entrypointowns discovery, parsing, naming, and normalized entrypoint descriptions. Reuse its finders instead of reaching into a feature plugin for the same information.src/cli/entrypoint/parserowns entrypoint schemas, defaults, and interpretation of parsed data. These classes, includingAbstractParser, configure file readers but must not traverse TypeScript AST or constructProgram/TypeCheckerinstances. Keep source analysis infileand its specializedfile/parsers; return plain metadata so each entrypoint decides what an export means.src/cli/pluginsowns framework feature policy and composition: interpreting entrypoint options, configuring the bundler, supplying normalized data, and declaring manifest requirements.src/cli/bundler/pluginsowns 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/entryand 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.
- A framework feature plugin must not import or instantiate another feature plugin's manager or implementation. For example, content uses
PageFinderfrom@cli/entrypoint, notPagefromplugins/page, to resolveisolation.pagealiases. - 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/utilsandbundler/plugins/types.tsfor shared plugin infrastructure such as compilation hooks and option contracts. Usebundler/utilsfor 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.
- 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
propertycontract. - Do not move build-only details into public domain types merely because a feature uses them. For example,
IsolatedStylesLayeris 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.tsexporting 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. DefinePluginis 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.
- The entrypoint asset map describes which files belong to an entrypoint. Keep
jsandcsscomplete; do not add feature-specific delivery subsets such asisolatedCssto 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, orframePageto 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.
- 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.
- Every built-in storage uses a namespace exactly matching its owning package name, including case. In this framework, use
PackageNamefrom@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.
- Import internal TypeScript modules through configured aliases or relative paths without a file extension. Do not write the future
.jsoutput 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?rawkeep 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 noindex.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.tsis its package subpathadnbn/entry/<name>. List its exports explicitly and do not useexport *, so an internal helper never becomes public by accident. - Apply this convention to new and changed imports; do not mass-rewrite unrelated files.
- Always write
ifandelsebodies as multiline blocks with braces, even for a singlereturn,throw, or expression. Never use inline guards such asif (!current()) return;. - Put a blank line before
returnwhen another statement precedes it in the same block. Areturnthat 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;- 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, notEntryPoint. - 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, andEntrypointAssetsMapEntry. - Suffixes must describe the actual role or shape:
Mapfor a keyed collection,MapEntryfor its value,Optionsfor configuration, andHandlerfor a callable handler. Do not use the same name for different data shapes. - Name classes after their responsibility and role, such as
LocaleFinderorContentParser. 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.
- 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"]orViewItem<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, useManifestHostPermissionsrather thanSet<string>,EntrypointFilerather than{file: string; import: string}, andViewItems<O>rather thanMap<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.tsdoes forchrome.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.
- 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 ReactLocaleinReactLocale.ts. - When a directory's
index.tsexposes 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.
- 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 asPromise<X>. A getter returning a collection should also express that collection in its name. - For example, use
getEntrypointAssets(): EntrypointAssetsandgetEntrypointAssetsMap(): EntrypointAssetsMap. - Add qualifiers such as
Current,Runtime, orBuildonly 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.
- Update declarations, imports, re-exports, callers, tests, fixtures, and examples together, including the root
addonplayground 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.
- Use PascalCase for a file whose primary export is a class or React component, matching that export's name:
LocaleFinder.ts,BuildAssetsMapPlugin.ts, andDiagnosticPanel.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, andContentManager.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, andentrypoint-assets.integration.test.ts. - Use lowercase kebab-case for multiword directory names, including directories containing classes or React components:
entrypoint-assetsanddiagnostic-panel. - Preserve tool-required conventional names such as
AGENTS.mdandREADME.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.