You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Produce the normative specification for the Freelens v2 extension API — the
single authoritative document an extension author (and the host maintainers)
can rely on to know what the API guarantees. It has two parts:
Contracts — the normative API surface and its invariants.
Migration howto (v1 to v2) — the developer-facing guide for porting an
existing v1 extension.
Today the only extension-author doc is docs/v2-extension-migration.md,
which is a migration guide (task-oriented "what changed and how to move"),
last aligned to the shipped React 19 / @ogre-tools/* 23 state in #2302. It
carries a lot of accurate cross-cutting constraints but deliberately leaves the
normative core as placeholders ("the concrete rename table ... will be appended
here", the namespace surface is not enumerated). This issue closes that gap.
Relationship to the existing guide: the migration howto part absorbs and
supersedesdocs/v2-extension-migration.md (fold its content in, do not
duplicate it), while the contracts part is new normative material. Final
filename to be decided during the work (e.g. docs/v2-extension-api.md as the
umbrella, or split contracts / howto — see Open questions).
Context: v2 broke v1 compatibility on purpose (see docs/v2-plan.md, decisions D2/D5).
Part of the v2 extension-API effort; follows up #2154 (React 17→19, done) and #2302 (doc accuracy). This issue is the spec itself, tracked separately.
Sequencing: several contracts here are downstream of #2400
Do not freeze Part 1 before #2400 settles, because some of the items below are not
independent guarantees — they are consequences of how an extension is delivered and
loaded, and they change when that changes.
The clearest case is the React contract. "Declare react/react-dom as peers ^19 and
do not bundle your own" holds today because pnpm resolves peers a particular way into a
particular node_modules tree. #2400 replaces that with a vendor-or-bundle contract and
no dependency resolution at all, at which point the same rule becomes a property of the
tarball rather than an emergent behaviour of a resolver. Same sentence, different reason,
different failure mode — and it can only be written once.
The module-format contract is affected the same way: #2400 blocks require() in the
renderer, which makes renderer entry points ESM-only and removes the latitude D2 currently
grants.
Writing these down before the mechanism is decided would record its accidents as
promises — which is the failure this whole effort has now caught three times at smaller
scale (#2395, #2399, #2400).
Sibling document: the ABI
#2401 specifies the binary side of the contract — what an extension may ship besides
JavaScript, and how it runs it. This issue answers what an extension may call; that one
answers what it may ship and execute. They are kept apart because an API mistake surfaces
as a type error and an ABI mistake surfaces as a binary that will not run, or as an
endpoint-protection alert. #2401 folds its result into the same specification document.
Part 1 — Contracts (normative)
Each contract must state the guarantee, the stable surface, and the failure
mode when violated. Source material for most of these already exists (verified)
in the migration guide and can be lifted with a normative reframing:
Packaging & publication.@freelensapp/extensions is the only
published package (types + runtime shim); every other @freelensapp/* is
private and must not be a direct dependency. (D4/D5)
Runtime-global API.globalThis.FreelensExtensionApi = { Common, Main }
(main) / { Common, Renderer } (renderer); the published package is a thin
re-export shim; wrong-process namespace is undefined at runtime while the
types expose the full surface. (D5)
Module format & loading. ESM or CommonJS accepted via the import()-based loader; the one restriction is no top-level await in the
entrypoint graph. (D2)
Namespace enumeration. What each of Common / Main / Renderer
actually provides (registrations, components, tokens, helpers) — the piece
the current guide does not enumerate.
Registration contracts.Renderer.Registrations (globalPages / clusterPages), sidebar items, and the other extension-facing injection
tokens, as normative contracts (shape, id semantics, lifecycle).
Dependency-injection surface.@ogre-tools/*23 leaks through the
API (tokens, getInjectable, React injection helpers). Namespaced
runtime-registered ids (<namespace>:<declaredId>) and where the host
strips them
(sidebar-items.injectable.ts).
React contract. Host-provided React 19, single instance via Renderer.React / Renderer.ReactDOM; extensions declare react/react-dom
as peers ^19 and must not bundle their own (invalid-hook-call trap).
Routing contract.react-router 5 re-exports removed; navigation via @freelensapp/routing + navigateToRoute; v5 path dialect preserved by the
in-house matchPath. (React upgrade — Phase 2: routing modernization #2261)
Styling / CSS contract. Host injects the extension's sibling <entry>.css / style.css; flexbox.scss removed; host Tailwind does not
reach extensions; "bring your own Tailwind" rules. (see docs/v2-styling.md)
HTTP. Implemented and merged in Give extensions a host-provided fetch and replace the DOM/undici fetch types with structural ones #2395; what remains is stating it here rather than leaving it implicit:
extensions get HTTP from the host — Main.Util.fetch (proxy, CA and allowUntrustedCAs preferences applied) and Renderer.Util.fetch — instead
of bundling a client. Two symbols rather than one, because the
implementations genuinely differ per process. Request and response types are
structural, so neither undici nor a DOM type is imposed on the consumer,
and undici leaves catalogs.extensions with the dispatcher field that
put it there.
Consumer toolchain floors.tsconfig requirements (skipLibCheck, lib, moduleResolution); electron as an optional peer for types.
The lib floor needs restating, and Give extensions a host-provided fetch and replace the DOM/undici fetch types with structural ones #2395 has the measurement: the published
declaration names ambient globals rather than defining them, so it
currently hard-requires lib.dom through FetchRequestInfo = RequestInfo —
one of the few fetch names @types/node does not declare. State the floor
per surface area, not as one number for the package — Give extensions a host-provided fetch and replace the DOM/undici fetch types with structural ones #2396 showed the two
diverge. With structural fetch types, the fetch surface needs lib.domor @types/node (already in the catalog), while the package as a whole still
requires lib.dom outright, because the React component types name DOM types
that nothing else declares. An extension that touches only Util.fetch and KubeApi therefore has a genuinely lower floor than one that renders a
component, and the spec should say so rather than quote the higher number to
everyone. Give extensions a host-provided fetch and replace the DOM/undici fetch types with structural ones #2395 also proposes making this a gate instead of a promise:
type-check dist/extension-api.d.ts on its own under lib: ["ES2025"], types: ["node"] in CI — noting that such a check passes only once the React
side is accounted for, so it likely needs to be scoped rather than
whole-artifact.
Versioning & compatibility policy. How the published @freelensapp/extensions semver maps to breaking API changes going forward
(the guarantee authors can plan against) — new material, not in the guide.
Part 2 — Migration howto (v1 to v2)
Developer-facing porting guide, consolidated from the existing migration guide:
Routing migration (two paths: internal navigation API, or bundle your own
react-router).
Styling migration: drop the ?inline + <style> workaround; migrate off flexbox.scss (the legacy-class → plain-CSS table); optional own-Tailwind.
Chart.js v2→v4 options migration.
API namespace rename table (v1 → v2) — the currently-empty table,
filled from the example-extension port.
End-to-end checklist.
Dependencies / validation vehicle
Per D5 and Phase 4/7 of docs/v2-plan.md, the spec is finalized against the
port of freelens-example-extension.
That port is the source of the namespace rename table and the proof that the
enumerated contracts are complete and correct. The spec can be drafted before
the port lands, but the rename table and the "namespace enumeration" contract
cannot be marked done until validated against it.
Out of scope
Changing the API surface itself — this issue documents the shipped v2 API,
it does not redesign it. Any change discovered as necessary during writing is
a separate issue.
The example-extension port itself (its own tracked work; this issue consumes
its result).
Acceptance criteria
A single authoritative spec document exists (contracts + howto), replacing docs/v2-extension-migration.md.
Every contract in Part 1 is stated normatively (guarantee, stable surface,
failure mode) and cross-checked against the code it describes.
The v1→v2 namespace rename table is filled from the example-extension port.
No remaining "will be appended here" / TODO placeholders in the shipped doc.
Links from docs/v2-plan.md and the routing docs point at the new spec.
Open questions
One umbrella document (docs/v2-extension-api.md with a contracts section and
a migration section), or two linked documents (spec + howto)?
Keep the docs/v2-* naming, or promote to a stable docs/extension-api.md
now that v2 is mainline?
References
docs/v2-plan.md decisions D2 (module format) and D5 (extension API)
Goal
Produce the normative specification for the Freelens v2 extension API — the
single authoritative document an extension author (and the host maintainers)
can rely on to know what the API guarantees. It has two parts:
existing v1 extension.
Today the only extension-author doc is
docs/v2-extension-migration.md,which is a migration guide (task-oriented "what changed and how to move"),
last aligned to the shipped React 19 /
@ogre-tools/*23 state in #2302. Itcarries a lot of accurate cross-cutting constraints but deliberately leaves the
normative core as placeholders ("the concrete rename table ... will be appended
here", the namespace surface is not enumerated). This issue closes that gap.
Relationship to the existing guide: the migration howto part absorbs and
supersedes
docs/v2-extension-migration.md(fold its content in, do notduplicate it), while the contracts part is new normative material. Final
filename to be decided during the work (e.g.
docs/v2-extension-api.mdas theumbrella, or split contracts / howto — see Open questions).
Context: v2 broke v1 compatibility on purpose (see
docs/v2-plan.md, decisions D2/D5).Part of the v2 extension-API effort; follows up #2154 (React 17→19, done) and
#2302 (doc accuracy). This issue is the spec itself, tracked separately.
Sequencing: several contracts here are downstream of #2400
Do not freeze Part 1 before #2400 settles, because some of the items below are not
independent guarantees — they are consequences of how an extension is delivered and
loaded, and they change when that changes.
The clearest case is the React contract. "Declare
react/react-domas peers^19anddo not bundle your own" holds today because pnpm resolves peers a particular way into a
particular
node_modulestree. #2400 replaces that with a vendor-or-bundle contract andno dependency resolution at all, at which point the same rule becomes a property of the
tarball rather than an emergent behaviour of a resolver. Same sentence, different reason,
different failure mode — and it can only be written once.
The module-format contract is affected the same way: #2400 blocks
require()in therenderer, which makes renderer entry points ESM-only and removes the latitude D2 currently
grants.
Writing these down before the mechanism is decided would record its accidents as
promises — which is the failure this whole effort has now caught three times at smaller
scale (#2395, #2399, #2400).
Sibling document: the ABI
#2401 specifies the binary side of the contract — what an extension may ship besides
JavaScript, and how it runs it. This issue answers what an extension may call; that one
answers what it may ship and execute. They are kept apart because an API mistake surfaces
as a type error and an ABI mistake surfaces as a binary that will not run, or as an
endpoint-protection alert. #2401 folds its result into the same specification document.
Part 1 — Contracts (normative)
Each contract must state the guarantee, the stable surface, and the failure
mode when violated. Source material for most of these already exists (verified)
in the migration guide and can be lifted with a normative reframing:
@freelensapp/extensionsis the onlypublished package (types + runtime shim); every other
@freelensapp/*isprivate and must not be a direct dependency. (D4/D5)
globalThis.FreelensExtensionApi = { Common, Main }(main) /
{ Common, Renderer }(renderer); the published package is a thinre-export shim; wrong-process namespace is
undefinedat runtime while thetypes expose the full surface. (D5)
import()-based loader; the one restriction is no top-levelawaitin theentrypoint graph. (D2)
Common/Main/Rendereractually provides (registrations, components, tokens, helpers) — the piece
the current guide does not enumerate.
Renderer.Registrations(globalPages/clusterPages), sidebar items, and the other extension-facing injectiontokens, as normative contracts (shape, id semantics, lifecycle).
@ogre-tools/*23 leaks through theAPI (tokens,
getInjectable, React injection helpers). Namespacedruntime-registered ids (
<namespace>:<declaredId>) and where the hoststrips them
(
sidebar-items.injectable.ts).Renderer.React/Renderer.ReactDOM; extensions declarereact/react-domas peers
^19and must not bundle their own (invalid-hook-call trap).react-router5 re-exports removed; navigation via@freelensapp/routing+navigateToRoute; v5 path dialect preserved by thein-house
matchPath. (React upgrade — Phase 2: routing modernization #2261)<entry>.css/style.css;flexbox.scssremoved; host Tailwind does notreach extensions; "bring your own Tailwind" rules. (see
docs/v2-styling.md)Renderer.Component.BarChart/PieChart; any other host-bundled surfacean extension consumes. The list itself is
catalogs.extensionsinpnpm-workspace.yaml, audited down from 21 entries to 17 in Prune the @freelensapp/extensions dependency surface before the v2 spec freezes it #2360.Its external-import counterpart — the third-party packages the bundled
extension-api.d.tsforces an author to resolve — is tracked in Enforce the extension API surface with an API report #2366 andpasted in here once the declaration builds again (@freelensapp/extensions build:dist is broken: TypeScript 7 removed the JS compiler API #2363).
extensions get HTTP from the host —
Main.Util.fetch(proxy, CA andallowUntrustedCAspreferences applied) andRenderer.Util.fetch— insteadof bundling a client. Two symbols rather than one, because the
implementations genuinely differ per process. Request and response types are
structural, so neither
undicinor a DOM type is imposed on the consumer,and
undicileavescatalogs.extensionswith thedispatcherfield thatput it there.
tsconfigrequirements (skipLibCheck,lib,moduleResolution);electronas an optional peer for types.The
libfloor needs restating, and Give extensions a host-provided fetch and replace the DOM/undici fetch types with structural ones #2395 has the measurement: the publisheddeclaration names ambient globals rather than defining them, so it
currently hard-requires
lib.domthroughFetchRequestInfo = RequestInfo—one of the few fetch names
@types/nodedoes not declare. State the floorper surface area, not as one number for the package — Give extensions a host-provided fetch and replace the DOM/undici fetch types with structural ones #2396 showed the two
diverge. With structural fetch types, the fetch surface needs
lib.domor@types/node(already in the catalog), while the package as a whole stillrequires
lib.domoutright, because the React component types name DOM typesthat nothing else declares. An extension that touches only
Util.fetchandKubeApitherefore has a genuinely lower floor than one that renders acomponent, and the spec should say so rather than quote the higher number to
everyone. Give extensions a host-provided fetch and replace the DOM/undici fetch types with structural ones #2395 also proposes making this a gate instead of a promise:
type-check
dist/extension-api.d.tson its own underlib: ["ES2025"],types: ["node"]in CI — noting that such a check passes only once the Reactside is accounted for, so it likely needs to be scoped rather than
whole-artifact.
@freelensapp/extensionssemver maps to breaking API changes going forward(the guarantee authors can plan against) — new material, not in the guide.
Part 2 — Migration howto (v1 to v2)
Developer-facing porting guide, consolidated from the existing migration guide:
package.json/tsconfig.jsonfor a v2 extension.ReactDOM.render,findDOMNode, legacy string refs).@ogre-tools/*17→23 re-check.react-router).
?inline+<style>workaround; migrate offflexbox.scss(the legacy-class → plain-CSS table); optional own-Tailwind.filled from the example-extension port.
Dependencies / validation vehicle
Per D5 and Phase 4/7 of
docs/v2-plan.md, the spec is finalized against theport of
freelens-example-extension.
That port is the source of the namespace rename table and the proof that the
enumerated contracts are complete and correct. The spec can be drafted before
the port lands, but the rename table and the "namespace enumeration" contract
cannot be marked done until validated against it.
Out of scope
it does not redesign it. Any change discovered as necessary during writing is
a separate issue.
its result).
Acceptance criteria
docs/v2-extension-migration.md.failure mode) and cross-checked against the code it describes.
docs/v2-plan.mdand the routing docs point at the new spec.Open questions
docs/v2-extension-api.mdwith a contracts section anda migration section), or two linked documents (spec + howto)?
docs/v2-*naming, or promote to a stabledocs/extension-api.mdnow that v2 is mainline?
References
docs/v2-plan.mddecisions D2 (module format) and D5 (extension API)@ogre-tools/*23) — merged