Imported from thednp/dommatrix (
AGENTS.md). Install upstream withnpx skills add thednp/dommatrix. Copyright stays with the author.
AGENTS.md
Guidance for AI agents working on this repository.
Project Overview
@thednp/dommatrix is a TypeScript shim for the native DOMMatrix interface. It ships a single default export, the CSSMatrix class, with a nearly identical API surface to native DOMMatrix, but works in Node.js and legacy browsers where DOMMatrix does not exist.
The library is intentionally small and dependency-free. There are no runtime dependencies; the dist/ bundle is produced from src/index.ts alone.
Tech Stack
- Language: TypeScript 7 (
typescript@^7),strictmode,moduleResolution: "Bundler" - Package manager: pnpm (
packageManager: pnpm@10.33.0) - Build: tsdown (rolldown-based), config in
tsdown.config.mts— produces ESM (dommatrix.mjs), CJS (dommatrix.cjs), UMD (dommatrix.js, globalCSSMatrix) and bundled types (dommatrix.d.ts) intodist/ - JSR:
deno.jsonmirrorspackage.json(name, version, MIT license, keywords) and publishes the raw TypeScript source viadeno publish. Keepversionin sync between the two files - Tests: Vitest 4 in browser mode only (Playwright + Chromium, headless), with istanbul coverage in
vitest.config.ts - Lint/format: Deno (
deno lint,deno fmt) — not ESLint/Prettier - Runtime requirement: Node >= 20, pnpm >= 8.6
Commands
pnpm build # tsdown build + copy dist/dommatrix.js to docs/
pnpm test # vitest browser tests (headless Chromium) + coverage
pnpm test-ui # vitest browser tests with UI
pnpm lint # deno lint src + tsc --noEmit
pnpm lint:ts # deno lint src
pnpm check:ts # tsc --noEmit
pnpm fix:ts # deno lint src --fix
pnpm format # deno fmt src
pnpm bench # vitest browser bench vs test/fixtures/index-3.0.x.ts (no coverage)
pnpm bench:native # vitest browser bench vs native DOMMatrix (no coverage)
pnpm bench:docs # run both bench suites (2x), write test/fixtures/bench-results.json, regenerate the BENCHMARK.md tables and the README ## Benchmarks summary
Always run pnpm lint and pnpm test after making changes.
Deno equivalents: deno task test, deno task lint, deno task check, deno task format (also usable as bare deno test-style commands: deno lint src, deno check src/index.ts, deno fmt src, deno doc src/index.ts, deno publish).
Project Structure
src/index.ts— the entire library (single file, ~1200 lines). TheCSSMatrixclass plus module-level helper functions (Translate,Rotate,fromString, etc.). Heavily documented with JSDoc.test/fixtures/index-3.0.x.ts— snapshot of the pre-3.1.0 implementation used as the benchmark baseline bytest/dommatrix.bench.test.ts. Excluded from coverage (seevitest.config.mts) and from JSR publish (only thepublish.includefiles ship). Included in thetsconfig.jsonincludesopnpm check:tscovers it.src/types.ts— exported types (JSONMatrix,Matrix,Matrix3d,PointTuple)test/dommatrix.test.ts— the full test suite (Vitest, browser mode)test/dommatrix.bench.test.ts— benchmark vs thetest/fixtures/index-3.0.x.tssnapshot: parity suite (every op must match the backup within1e-9, runs inpnpm testand gates CI) + interleaved perf suite (auto-skipped under coverage)test/native.bench.test.ts— benchmark vs nativeDOMMatrix: parity suite (22 ops at1e-9, runs inpnpm test) + interleaved perf suitetest/fixtures/bench-results.json— last measured results, written byscripts/update-benchmark.js; single source of truth for theBENCHMARK.mdtables and the README## Benchmarkssummaryscripts/update-benchmark.js— runs both bench suites (2x, ~1 min), parses theafterAlltables from vitest stdout, writes the fixture, rewrites the two table blocks ofBENCHMARK.md(between the<!-- b1 -->/<!-- /b1 -->and<!-- b2 -->/<!-- /b2 -->markers, plus the<!-- b-updated -->date stamp) and regenerates the README## BenchmarkssummaryBENCHMARK.md— the full benchmark report (static methodology intro,<!-- b1 -->/<!-- b2 -->-marked table blocks, static conclusion); only the marked blocks are regenerated bypnpm bench:docstest/fixtures/— test helpers, sample data, and the bench-results fixturedocs/— GitHub Pages demo;docs/dommatrix.jsis a build artifact copied fromdist/dist/— build output, committed to the repo; regenerate withpnpm build, never edit by handexperiments/— archived Cypress experiments (not part of the build)
Code Conventions
- Single default export:
export default class CSSMatrix— there are no runtime named exports. The helper types (Matrix,Matrix3d,JSONMatrix,PointTuple,CSSMatrixInput) are re-exported from./types.tsas type-only exports (export type { ... } from "./types.ts") - Style: Deno-style — no semicolons, double quotes, 2-space indent, trailing commas.
deno lintanddeno fmt srcenforce this; do not fight the formatter - No comments unless they are JSDoc — the codebase uses JSDoc extensively on public API, matching the existing style
- Naming:
m11-m44are the canonical 3D values;a-fare the 2D aliases (getters/setters sync both) - API design: mirrors native
DOMMatrix— immutable methods (translate(),rotate(),scale(),skew(),multiply()) return new matrices;*Selfvariants mutate and returnthis. Static helpers (fromString,fromArray,fromMatrix,Translate,Rotate, ...) live on the class as static properties - Error style: throw
TypeErrorwith messages prefixedCSSMatrix: ...(tests assert on exact messages) is2D/isIdentityare computed getters (from matrix values), not construction-time flags
Critical Rules (do not violate)
- No runtime references to browser-only globals (
DOMMatrix,DOMPoint,CSSMatrix) without atypeof ... !== "undefined"guard. The library must work in Node.js where these globals do not exist. Theinstanceofguards inisCompatibleObject()andtransformPoint()are the canonical pattern:typeof DOMMatrix !== "undefined" && object instanceof DOMMatrix - Tests run in a browser, so a missing global guard will NOT fail tests on its own. The test suite simulates Node.js with
vi.stubGlobal("DOMMatrix", undefined)/vi.stubGlobal("DOMPoint", undefined)in the"Node.js Environment Test"describe block — keep that block covering any code path that touches these globals. - Coverage must stay 100% (statements, branches, functions, lines). The CI gate enforces it via istanbul. Every new branch needs a test.
- Do not edit
dist/ordocs/dommatrix.jsdirectly — they are build outputs; runpnpm build. - Do not add runtime dependencies. This is a zero-dependency library;
src/must stay import-free. - Do not change the dist file names (
dommatrix.cjs/.js/.mjs/.d.ts+ maps) — package.jsonexports,main,module, and the docs copy script depend on them.
Releasing (patch/minor/major)
- Bump
versioninpackage.jsonand indeno.json - Add an entry to
CHANGELOG.md(Keep a Changelog style, date-stamped) - Run
pnpm build && pnpm lint && pnpm testanddeno publish --dry-run --allow-dirty - Commit, then tag with the bare version number (no
vprefix — existing tags are1.0.0...3.1.0)
The GitHub Actions publish.yml workflow publishes to both npm and JSR on GitHub Release (JSR via OIDC when enabled on the scope, otherwise the JSR_TOKEN secret).
Note: prepublishOnly runs pnpm up --latest (updates all deps), pnpm format, pnpm lint, pnpm build — expect dependency bumps to land in the same commit if publishing.
Gotchas
- The old
vite.config.tswas replaced bytsdown.config.mtsin 3.0.5 —tsdown.config.mtsrelies on: object-formentryfor the chunk name (dommatrix), anoutputOptionsoverride for UMD (it must spread defaults, otherwisesourcemapis silently dropped), andoutExtensionsto keep the.d.tsfilename. The.mtsextension (not.ts) prevents Node'sMODULE_TYPELESS_PACKAGE_JSONESM-reparse warning —package.jsondeliberately has no"type": "module"because that would break Noderequire()of the UMDdist/dommatrix.js - With
deno.jsonpresent,deno lintenforcesverbatim-module-syntax— type-only imports (likeimport type CSSMatrix from "."insrc/types.ts) must useimport type - Vitest runs only in browser mode; any Node-only concern (globals,
require(),process) needs the stubbed-global test pattern - Browser
console.tableis NOT forwarded to the terminal — both bench suites accumulate rows onglobalThis(__CSSMATRIX_BENCH__,__CSSMATRIX_NATIVE_BENCH__, inspectable inpnpm test-uidevtools) and print a pad-alignedconsole.logtable inafterAll.console.loginafterAlldoes forward (but only with the verbose reporter — see next bullet). - The
BENCHMARK.mdtables are generated bypnpm bench:docs(scripts/update-benchmark.js) fromtest/fixtures/bench-results.json— do not hand-edit the blocks between the<!-- b1 -->/<!-- /b1 -->and<!-- b2 -->/<!-- /b2 -->markers (the tables and their interpretation only), nor the<!-- b-updated -->/<!-- /b-updated -->date stamp; the intro and the closing conclusion are otherwise static. The script also regenerates the short README## Benchmarkssummary — a "X–Yx faster" report with a last-updated line, linking toBENCHMARK.md— by writing between the<!-- b-summary -->/<!-- /b-summary -->markers of the section bounded by the## Benchmarksand## Demoheadings (the script fails if that boundary is not found). It spawns vitest with--reporter=verbose(the default reporter does not forward browserconsole.log), parses the twoafterAlltables from stdout, runs each suite twice and reports medians. Grouping thresholds: vs-3.0.x buckets at 6x / 2x / 1.12x (ratio >= 6 “Large”, >= 2 “Moderate”, >= 1.12 “Mild”, else “At parity”); native is split three-way at ±5% (the measured noise floor). - Bench measurements must run without coverage: istanbul instruments
src/index.tsbut nottest/fixtures/index-3.0.x.ts, so under coverage the current file measures 30-60% slower than an identical backup. The suites skip timing at runtime whentypeof globalThis["__VITEST_COVERAGE__"] === "object"(the key only appears after instrumented code executes) — parity assertions still run inpnpm test. - Interleaved sampling:
bench(a, b, iterations)warms up both implementations, then takes 2 alternating samples each and reports medians. Sequential per-implementation measurement biases toward the first implementation (~20-30%) because the second's allocations raise GC pressure. Identical files must measure ~0.92-1.09x. - Native DOMMatrix quirks that shaped
test/native.bench.test.ts: Chrome lacksDOMMatrix.fromArray(useDOMMatrix.fromMatrix(values)); native has noskew()/skewSelf()(onlyskewX/skewY);rotateAxisAngle(x, y, z, angle)takes the angle last;DOMPointexposesx/y/z/was prototype getters soObject.keys()is empty — extract values into a plain object before comparing; the bench test needs a long timeout (120s) because 23 interleaved cases take ~15s. rotateAxisAngle/rotateAxisAngleSelfthrow only when any of the 4 values is non-finite; zero-length vector returns a copy (orthisfor theSelfvariant)deno lintruns onsrc/only — thetsconfig.jsonincludeis["src/*"],noEmit: true