Skip to content

v1: freeze the API surface, move to Biome, update every dependency - #77

Merged
arshad-shah merged 6 commits into
mainfrom
claude/issue-35-v1-readiness-tbcdeq
Sep 1, 2026
Merged

arshad-shah merged 6 commits into
mainfrom
claude/issue-35-v1-readiness-tbcdeq

Conversation

@arshad-shah

Copy link
Copy Markdown
Owner

Summary

Closes the remaining pre-v1 gates from #35 and brings the whole dependency tree to latest, so 1.0.0 can ship off a clean build. Adds a major changeset — there are no breaking changes from 0.6.x; what v1 adds is the promise that the documented surface stays put.

Type of change

  • New feature (non-breaking)
  • Performance / refactor (no behaviour change)
  • Documentation only
  • Test or tooling only

Linked issues

Closes #35. Supersedes #44, and the dependabot PRs #49, #50, #51, #52, #53, #54, #62, #63, #64, #69, #70, #71, #72, #73, #74, #75, #76.

Contains the commit from #67 — merge that one first and this fast-forwards over it cleanly.

Implementation notes

API freeze

New API stability page assigns every export, command, flag, config field and runtime global to a tier: Stable, Experimental, or Internal. If it isn't listed there, it's internal. Symbols exported only because the CLI needs them across module boundaries (createBuildContext, classifyChange, generateHMRClientCode, writeManifest, PluginRunner) now carry @internal tags that say what to use instead.

The issue's three open questions, answered:

  • extforge/testing ships experimental. The Chrome fakes mirror a surface MV3 keeps extending, so freezing them means either an incomplete mock forever or a major release every time Chrome adds an API. They're also the lowest-blast-radius thing to change — a fake that shifts breaks a test suite, not a shipped extension.
  • HMR globals split. __EXTFORGE_HMR_QUIET__ is a documented user opt-out, so it's stable. __EXTFORGE_HMR__ is the binding target the dev-mode transform emits against and moves with the wire protocol, so it's internal.
  • extforge/plugins is stable. It already carries an apiVersion: 1 discriminator, so an incompatible future plugin API can ship as apiVersion: 2 alongside rather than breaking existing plugins.

Node policy is written down too: >=22.12, supported through Active and Maintenance LTS, and dropping an EOL major is a minor release — holding the floor down until the next major would mean shipping against an unpatched runtime.

ESLint + Prettier → Biome

One tool for both linting and formatting. Every ESLint suppression is ported to its Biome equivalent with the original rationale intact; the no-console whitelist survives as per-file overrides in biome.json. Rule levels are deliberately kept in line with the old config (noExplicitAny warns; noNonNullAssertion and noConfusingVoidType are off, because the codebase uses both idioms on purpose), so the result is 0 errors and the same 59 any warnings as before rather than a new wall of noise.

Biome's stricter preset did surface real things, fixed here rather than silenced:

  • Storage's private useChrome()hasChromeStorage(). It is not a React hook; only its name made useHookAtTopLevel think so.
  • getLogger() and the SWC refresh-plugin getters no longer memoize via assignment-in-expression.
  • forEach callbacks in tests no longer return a value.
  • An unused import and loop binding in tests/manifest.test.ts, and a void-returning return in the config reference generator.
  • The "we deliberately exclude defaultValue from deps" note in storage/react.ts becomes a real suppression — ESLint never had the react-hooks plugin installed, so that rule was never actually enforced.

Dependencies

Every npm dependency across the root package, docs-site, tests-e2e and the examples is on its latest release, plus all SHA-pinned actions (checkout v7, setup-node v6.4, codeql v4.36.2, pnpm/action-setup).

Two judgment calls worth a look:

  • TypeScript stays on 6.0.3. TS 7 typechecks the project cleanly, but breaks the .d.ts bundle: tsup's rollup-plugin-dts binds to the TypeScript compiler API, which the Go-based TS 7 doesn't expose. typescript-eslint also caps at <6.1.0, though Biome makes that moot.
  • Peer dependency ranges are untouched. They're compatibility floors for consumers, not versions to keep current — raising react >=18.0.0 to ^19 would drop React 18 users for no reason.

Astro 6 → 7 needed a Starlight migration: 0.39 removed the { label, autogenerate } sidebar shorthand, so those entries are nested under items now. Astro/Starlight also pull vulnerable postcss, nanoid, js-yaml and svgo transitively, which would fail the pnpm audit --prod gate — new pnpm.overrides pin them to patched versions, following the pattern already used for esbuild and devalue.

Cross-browser gate

pnpm check:cross-browser builds all three examples for Chrome, Firefox, Edge and Safari and asserts each emitted manifest is well-formed and carries that browser's shape (Firefox gets background.scripts + browser_specific_settings; the others get service_worker and no addon id). All 12 combinations pass, and it runs as its own CI job.

This is a build gate, not a behavioural one. Live-browser e2e is still Chromium-only — see below.

Changelog

Removed the stale [Unreleased] section. Everything in it (HMR v3, SWC + React Fast Refresh, content-script HMR scaffolding, centralized logging) shipped in 0.3.0 and 0.4.0, so it was advertising released work as pending.

Testing

  • pnpm typecheck && pnpm lint && pnpm test && pnpm build passes locally — 519 tests, 57 files
  • pnpm audit --prod reports 0 vulnerabilities
  • pnpm docs:build passes on Astro 7 / Starlight 0.41 — 50 pages
  • pnpm check:cross-browser passes — 3 examples × 4 targets
  • E2E not verified locally. Playwright 1.62 needs a browser build this sandbox can't fetch (cdn.playwright.dev is blocked by the environment's network policy). CI installs it, so the e2e job will be the first real signal. The jump is 1.59.11.62.1, so the risk is low, but it is unverified.

Documentation

  • Added reference/stability.mdx and guides/migration-v1.mdx, both wired into the sidebar
  • pnpm docs:gen re-run as part of docs:build
  • Added @internal TSDoc to the newly-classified internal surface
  • Added a major changeset

Breaking change notes

None. No config field was renamed, no export removed, no CLI flag changed between 0.6.x and 1.0.0. The migration guide leads with exactly that, then covers what does bite people skipping releases: the Node 22.12 floor and the logger's number formatting, both from 0.6.0.

The one contributor-facing change is pnpm lint now running Biome instead of ESLint. CONTRIBUTING.md is updated.

Checklist

  • No new runtime dependencies in src/core/@biomejs/biome is a devDependency
  • Followed the rules in CONTRIBUTING.md

Still open after this

Two gates from #35 aren't closed by code, flagged rather than quietly dropped:

  • Soak period — "one minor cycle with no new fix-* changesets against core". A process gate; 0.6.0 shipped with zero pending changesets, so the clock is running, but it's your call whether it's served.
  • Cross-browser e2e — this PR covers builds for all four targets. Real browser automation is Chromium-only. Firefox is tractable via Playwright and would be the natural next step; Safari needs a macOS runner plus web-extension conversion, and Edge is Chromium anyway.

Both nice-to-haves (performance baseline, bundle size budget) are also still open.


Generated by Claude Code

claude added 3 commits June 15, 2026 08:44
Broaden CSS support from the fixed 'tailwind' | 'vanilla' | 'none' enum to
an extensible pipeline any styling toolchain can plug into.

Core:
- New src/core/builder/css.ts defines CssProcessor / CssTransform /
  CssTransformContext and a processStylesheet pipeline (base processor →
  onCssTransform plugin chain → write). Commands run without a shell and
  fall back to a copy on failure.
- config.css now accepts a custom processor object (inline transform or CLI
  command) in addition to the three presets; schema validated.
- New onCssTransform plugin hook (types, runner, fireCssTransform). CSS types
  re-exported from both extforge and extforge/plugins.
- Presets behave correctly: 'vanilla'/'none' copy through instead of
  invoking the Tailwind CLI when it happens to be installed.
- Scaffold: non-tailwind css no longer ships @tailwind directives in
  globals.css (new globals.vanilla.css.tpl).

Docs:
- New Styling guide; updated configuration + plugin guides, README, index,
  and the auto-generated config reference (now handles union types and
  safely quotes frontmatter descriptions).

Tests: css pipeline + onCssTransform chain + schema cases (519 passing).

https://claude.ai/code/session_01NxAV3rCRLn29a7JZSkRACq
Toolchain
- Swap ESLint (flat config + typescript-eslint) and Prettier for Biome
  2.5.11, which handles both linting and formatting in one pass. Every
  ESLint suppression is ported to its Biome equivalent with the original
  rationale preserved; the `no-console` whitelist survives as per-file
  overrides in biome.json.
- Rules kept deliberately in line with the previous config: noExplicitAny
  warns (as before), noNonNullAssertion and noConfusingVoidType are off
  because the codebase uses both idioms on purpose.
- Formatter settings match the code as written (single quotes, semicolons,
  2-space indent, 100-column width), so the reformat is mechanical.

Genuine findings fixed along the way
- Storage's private `useChrome()` renamed to `hasChromeStorage()`. It is
  not a React hook; only its name made it look like one.
- `getLogger()` and the SWC refresh-plugin template getters no longer
  memoize via assignment-in-expression.
- `forEach` callbacks in tests no longer return a value.
- Dropped an unused `Browser` import and an unused loop binding in
  tests/manifest.test.ts, and a void-returning `return` in the config
  reference generator.
- The `defaultValue`-excluded-from-deps note in storage/react.ts becomes a
  real suppression now that the rule is actually enforced; ESLint never
  had the react-hooks plugin installed.

Dependencies
- Every npm dependency moved to its latest release across the root
  package, docs-site, tests-e2e and the examples. This supersedes the
  open dependabot PRs (#49-#54, #62-#64, #69-#76).
- TypeScript stays on 6.0.3. TypeScript 7 typechecks the project cleanly
  but breaks the .d.ts bundle: tsup's rollup-plugin-dts binds to the
  TypeScript compiler API, which the Go-based TS 7 does not expose.
- Peer dependency ranges are untouched. They are compatibility floors for
  consumers, not versions to keep current.
- Starlight 0.39 removed the `{ label, autogenerate }` sidebar shorthand;
  docs-site/astro.config.mjs migrates to nested `items`.
- New pnpm overrides pin postcss, nanoid, js-yaml and svgo to patched
  versions. Astro/Starlight pull vulnerable copies transitively, which
  would otherwise fail the `pnpm audit --prod` CI gate.

CI
- All SHA-pinned actions bumped: actions/checkout v7.0.0,
  actions/setup-node v6.4.0, github/codeql-action v4.36.2, and
  pnpm/action-setup to its current v4.4.0 commit.
- dependabot.yml drops the eslint/typescript-eslint groups for a biome one.

Verified: typecheck, lint, 519 tests, tsup build, docs build, and
`pnpm audit --prod` (0 vulnerabilities) all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXZCPnbtT51ASiFHLMXQTZ
Closes the remaining pre-v1 gates from #35.

API freeze
- New reference page assigns every export, command, flag, config field and
  runtime global to a tier: Stable, Experimental, or Internal. If it isn't
  listed there, it's internal.
- Tagged the symbols that are exported only because the CLI needs them
  across module boundaries: createBuildContext, classifyChange,
  generateHMRClientCode, writeManifest, and PluginRunner. Each `@internal`
  tag says what to use instead.
- Answers the three open questions from the issue:
  - `extforge/testing` ships **experimental**. The Chrome fakes mirror a
    surface MV3 keeps extending, so freezing them would mean either an
    incomplete mock forever or a major release per Chrome API. They're also
    the lowest-blast-radius thing to change — a test suite breaks, not a
    shipped extension.
  - HMR globals split. `__EXTFORGE_HMR_QUIET__` is a documented user opt-out,
    so it's stable. `__EXTFORGE_HMR__` is the binding target the dev-mode
    transform emits against and moves with the wire protocol, so it's internal.
  - `extforge/plugins` is **stable**. It already carries an `apiVersion: 1`
    discriminator, so an incompatible future plugin API can ship as
    `apiVersion: 2` alongside rather than breaking existing plugins.
- Node policy written down: >=22.12, supported through Active and Maintenance
  LTS, and dropping an EOL major is a minor release — holding the floor down
  until the next major would mean shipping against an unpatched runtime.

Cross-browser gate
- `pnpm check:cross-browser` builds all three examples for Chrome, Firefox,
  Edge and Safari and asserts each emitted manifest is well-formed and
  carries that browser's shape (Firefox gets `background.scripts` and
  `browser_specific_settings`; the others get `service_worker` and no
  addon id). All 12 combinations pass.
- Runs as its own CI job. This is a build gate, not a behavioural one:
  live-browser e2e is still Chromium-only.

Docs and changelog
- Migration guide for 0.x → 1.0. There is nothing to codemod, so it leads
  with that and covers what actually bites: the Node 22.12 floor and the
  logger's number formatting, both from 0.6.0.
- Removed the stale `[Unreleased]` section. Everything in it — HMR v3, SWC
  + React Fast Refresh, content-script HMR scaffolding, centralized logging
  — shipped in 0.3.0 and 0.4.0, so it was claiming released work was pending.

Verified: typecheck, lint, 519 tests, build, docs build, `pnpm audit --prod`
(0 vulnerabilities) and the new cross-browser check all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXZCPnbtT51ASiFHLMXQTZ
Comment thread scripts/gen-plugin-reference.ts Fixed
Comment thread scripts/gen-plugin-reference.ts Fixed
Comment thread scripts/gen-plugin-reference.ts Fixed
…ation

CodeQL flagged three high-severity "incomplete string escaping" alerts in
the plugin reference generator, and it is right.

`escapeType` and `escapeDoc` added backslashes without escaping backslashes
first. Given input that already contains one, the output is wrong:

  escapeType('a\|b')  ->  'a\\|b'

Markdown reads `\\` as a literal backslash, which leaves the `|` as an
unescaped cell delimiter and breaks the table row. For `escapeDoc` the
failure is worse: `a\{b` emits `a\\{b`, and an unescaped `{` in MDX opens a
JSX expression, so the docs build fails outright.

Escaping backslashes first fixes both. Nothing in the current plugin types
or JSDoc contains a backslash, so the generated output is byte-identical —
verified by diffing docs-site/src/content/docs/reference/plugins before and
after. This is hardening against the input that would break it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXZCPnbtT51ASiFHLMXQTZ
Comment thread scripts/gen-plugin-reference.ts Fixed
Comment thread scripts/gen-plugin-reference.ts Fixed
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 1, 2026

Copy link
Copy Markdown

Deploying extforge with  Cloudflare Pages  Cloudflare Pages

Latest commit: a4e4c6f
Status: ✅  Deploy successful!
Preview URL: https://f9e90b1f.extforge.pages.dev
Branch Preview URL: https://claude-issue-35-v1-readiness.extforge.pages.dev

View logs

Picks up #67, which squash-merged to main as 2bda0bc. This branch already
contained that work (commit 34955a9, merged here earlier), so the squash
produced conflicts against the Biome-formatted copies rather than any real
divergence.

Verified `git diff 34955a9 origin/main` is empty before resolving — main
carries nothing this branch lacks — so every conflict was resolved in favour
of this branch, whose files are the same content with Biome's import ordering
and the new @internal tags on top.

Git auto-merged src/core/plugins/index.ts without flagging a conflict and
duplicated the CSS type re-export block, which broke typecheck with
TS2300. Deduped.

The merged tree is byte-identical to the pre-merge head (594213a), which is
the expected invariant given main added nothing new.

Verified: typecheck, lint, 519 tests, build, docs build, cross-browser check,
and `pnpm audit --prod` all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXZCPnbtT51ASiFHLMXQTZ
CodeQL still flagged escapeType/escapeDoc after the previous commit. The
two-pass form was functionally correct — it escaped backslashes before
delimiters — but the backslash pass lived in a separate helper, and
js/incomplete-sanitization does not follow the escaping across that call, so
it still read each `.replace(/\|/g, ...)` as escaping without handling
backslashes.

Folding the backslash into the character class fixes it for both readers.
One pass over `[\\|]` (and `[\\{}]`) escapes the backslash alongside the
delimiter, so there is no ordering to get wrong and no cross-function
dataflow for the query to lose track of.

Verified equivalent to the two-pass version across plain input, bare
backslashes, bare delimiters, backslash-delimiter pairs and doubled
backslashes. Generated output under docs-site reference/plugins is
byte-identical again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXZCPnbtT51ASiFHLMXQTZ
@arshad-shah
arshad-shah merged commit 2a3eb1b into main Sep 1, 2026
10 checks passed
arshad-shah pushed a commit that referenced this pull request Sep 1, 2026
Release extforge@1.0.0.

Major: the API stability contract (#77).
Minor: the extensible CSS pipeline (#67).
arshad-shah added a commit that referenced this pull request Sep 1, 2026
The 1.0.0 and 1.1.0 releases published to npm but created no git tag and
no GitHub release, while the workflow reported success.

changesets/action v1 detects what was published by regex-matching
`changeset publish` stdout for `New tag: <pkg>@<version>`. #77 upgraded
@changesets/cli to v3, whose clack-style output no longer prints that
line, so the action concluded nothing had been published and skipped
both the tag push and the release creation without failing. Tags were
created on the runner and discarded with it — which is why
`git ls-remote --tags` still stops at 0.6.0.

Upstream replaced stdout parsing with a CHANGESETS_OUTPUT file in v2
(changesets/action#678) and v2 now rejects the CLI v2/action v1 mismatch
outright (changesets/action#699).

v2 renamed every input, so this is not a bare SHA bump:
version -> version-script, publish -> publish-script,
commit -> commit-message, title -> pr-title.

Both env vars are dropped. GITHUB_TOKEN is no longer read from the
environment (changesets/action#674); the `github-token` input defaults to
`github.token`, which is what was being passed. NPM_TOKEN is no longer
used to write an .npmrc (changesets/action#695) — publishing already goes
through OIDC trusted publishing, confirmed by the SLSA provenance
attestation on the published 1.1.0 tarball.

v2 pushes commits and tags through the GitHub API rather than the git
CLI, so `persist-credentials: false` stays safe and tags are signed with
GitHub's GPG key.


Claude-Session: https://claude.ai/code/session_01AZSFjNFjuoeXUjuFkA6Cha

Co-authored-by: Arshad shah <arshad.shah@hmhco.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Track v1.0.0 readiness

3 participants