diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 9d2191dd..50ec8d5a 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -4,6 +4,13 @@ on:
release:
types: [published]
+# npm provenance is signed with a GitHub OIDC token, so the job has to be
+# allowed to mint one. Without id-token: write the publish fails rather than
+# quietly falling back to an unsigned package.
+permissions:
+ contents: read
+ id-token: write
+
jobs:
publish:
runs-on: ubuntu-latest
@@ -42,9 +49,9 @@ jobs:
- name: Publish to npm
run: |
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
- npm publish --access public --tag beta
+ npm publish --access public --tag beta --provenance
else
- npm publish --access public
+ npm publish --access public --provenance
fi
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d5c9b472..fffa2dda 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,32 @@
+## 1.10.0 (2026-09-06)
+
+* feat(matchLayout): layout snapshots, in the browser, with no SaaS account and no containers (#337) ([8ab683e](https://github.com/BRIKEV/twd/commit/8ab683e)), closes [#337](https://github.com/BRIKEV/twd/issues/337)
+* feat(matchLayout): `twd.matchLayout(el, name)` captures a DOM node to an anchored grid of bits and compares it against a committed `.snap` reference, throwing when the geometry moved. It watches **geometry, not appearance**: a changed string or a shifted colour is invisible to it on purpose, because `twd.should` already covers content
+* feat(matchLayout): the reference is a text file you commit, with an ASCII preview inside it, so a layout change is reviewable in a pull request without opening an image
+* feat(matchLayout): a new `twdSnapshot()` Vite plugin does the file I/O, since a browser cannot. It holds no snapshot policy: every decision lives in `matchLayout`
+* feat(matchLayout): snapshots are **skipped in the sidebar by default**. The sidebar resizes the page and a developer's window is an arbitrary size, so a reference created there would fail for everyone else. `twdSnapshot({ debug: true })` opts in locally, and `twd-cli` 1.6.0 or newer is what actually decides a snapshot
+* feat(sidebar): an execution speed selector, and pacing state moved somewhere every bundle can see it. `pace.ts` kept the value in module scope, but the module ships in more than one bundle, so a pace set from the sidebar never reached the copy the runner reads (#340) ([1e701be](https://github.com/BRIKEV/twd/commit/1e701be)), closes [#340](https://github.com/BRIKEV/twd/issues/340)
+* feat(diagnostics): a failing test now reports the route it was on and which mock rules never fired, as plain data the reporter renders (#335) ([a43bd9d](https://github.com/BRIKEV/twd/commit/a43bd9d)), closes [#335](https://github.com/BRIKEV/twd/issues/335)
+* fix(matchLayout): externalise `node:` prefixed builtins so `twdSnapshot` loads at all. `rollupOptions.external` listed only the unprefixed `fs` and `path`, so rolldown bundled them and emitted broken interop, and any app using the plugin died on startup with `l.default.resolve is not a function` (#339) ([6470a11](https://github.com/BRIKEV/twd/commit/6470a11)), closes [#339](https://github.com/BRIKEV/twd/issues/339)
+* fix(matchLayout): the failure message says where the layout moved again, with the diff map restored
+* feat: **`twd.viewport()` and `twd.resetViewport()` are removed** (#338) ([e475acf](https://github.com/BRIKEV/twd/commit/e475acf)), closes [#338](https://github.com/BRIKEV/twd/issues/338)
+* docs: a Layout Snapshots (beta) page, and the API reference entry for `matchLayout`
+* chore: dependencies (14 dependabot bumps)
+
+**Removal, and why this is not a major.** `twd.viewport()` shipped in 1.6.0 as a
+beta and did not survive contact with real users. It simulated a viewport by
+constraining the body, overriding `innerWidth`, `innerHeight` and `matchMedia`,
+and rewriting CSS `@media` rules by hand at runtime, and that machinery did not
+hold up in practice. Anyone calling `twd.viewport()` or `twd.resetViewport()`
+has to drop those calls. We are not cutting a major for it because usage of the
+command is effectively nil, and we would rather start the responsive story from
+a clean slate than keep a method that makes people think the problem is solved.
+
+**Layout snapshots ship as a beta feature.** They are strictly additive: without
+`matchLayout` in a test nothing changes, and in the sidebar they are skipped
+unless you ask for them. What may still change is the `.snap` format and the
+API. Deciding a snapshot needs `twd-cli` 1.6.0 or newer.
+
## 1.9.0 (2026-07-28)
* feat(pace): command pacing for recorded runs (#316) ([3aeceb4](https://github.com/BRIKEV/twd/commit/3aeceb4)), closes [#316](https://github.com/BRIKEV/twd/issues/316)
diff --git a/package-lock.json b/package-lock.json
index 4c16bcab..eb64000b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "twd-js",
- "version": "1.9.0",
+ "version": "1.10.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "twd-js",
- "version": "1.9.0",
+ "version": "1.10.0",
"license": "MIT",
"dependencies": {
"@testing-library/dom": "^10.4.1",
@@ -140,6 +140,7 @@
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.7",
@@ -593,6 +594,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -641,6 +643,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -1979,6 +1982,7 @@
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
@@ -2158,6 +2162,7 @@
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -2168,6 +2173,7 @@
"integrity": "sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -2192,6 +2198,7 @@
"integrity": "sha512-WASHDpCm6qO5jj9g1a+8NiW5+GCkAyLReR56/4VruYmNgfUmqpxOfZ2Yfb8xGfJPWv5Qi6LSD8sXdces3vbp/Q==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.68.0",
@@ -2231,6 +2238,7 @@
"integrity": "sha512-fHq2VC1kpyYfvEcbiMjOpySY4WS7voEp89yAThrHRX5sm9j2lzYppCb2umFMEed4fWcyeLjHxrz0mpjNBaBxMQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.68.0",
"@typescript-eslint/types": "8.68.0",
@@ -2485,6 +2493,7 @@
"integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@bcoe/v8-coverage": "^1.0.2",
"@vitest/utils": "4.1.10",
@@ -2957,6 +2966,7 @@
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -3148,6 +3158,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.11.20",
"caniuse-lite": "^1.0.30001810",
@@ -3323,6 +3334,7 @@
"integrity": "sha512-cs+LadpH7Kpw0M3k8wurk+sOVVDAENA0iK4OBOrkL94j5lEVYRJ4j3zd2bhY9qgzyrPqthdcYT3axzRN7AliMg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=22"
}
@@ -3333,6 +3345,7 @@
"integrity": "sha512-O+x4N2yH+ijvqWlIyTHsXTAP+algNWgGbjY2duCe8w2vUMvUB95cLRslCPfTMQyLAKlet3bhZTdu6ozn4M+QJQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@simple-libs/stream-utils": "^2.0.0",
"argue-cli": "^3.1.0"
@@ -3615,6 +3628,7 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
+ "peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@@ -3679,6 +3693,7 @@
"integrity": "sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"workspaces": [
"packages/*"
],
@@ -4014,6 +4029,7 @@
"integrity": "sha512-qV0g8hRYBqgACcFOH3f9wXc4zPKhr/0z9RI2a6ZijZ72EeBi4g8oBy8zAWuUR1TsMpOzwpUMFvjdasrC41Joug==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"tabbable": "^6.5.0"
}
@@ -5265,6 +5281,7 @@
"resolved": "https://registry.npmjs.org/preact/-/preact-10.29.8.tgz",
"integrity": "sha512-ej2aVZ+vZ8WO7tvlQWRM9N63A0KzF9q4mWJfDUHgYaIofWY9hu74QdnQrjoPMmZi2/nZ5gN0bJCQF49xQqx09Q==",
"license": "MIT",
+ "peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
@@ -5442,6 +5459,7 @@
"integrity": "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@oxc-project/types": "=0.143.0",
"@rolldown/pluginutils": "^1.0.0"
@@ -5486,8 +5504,7 @@
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/semver": {
"version": "6.3.1",
@@ -5802,6 +5819,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -6071,6 +6089,7 @@
"integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"lightningcss": "^1.33.0",
"picomatch": "^4.0.5",
@@ -6246,6 +6265,7 @@
"integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@vitest/expect": "4.1.10",
"@vitest/mocker": "4.1.10",
@@ -6343,6 +6363,7 @@
"integrity": "sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.41",
"@vue/compiler-sfc": "3.5.41",
diff --git a/package.json b/package.json
index c7dc9567..2f9e7fc1 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "twd-js",
- "version": "1.9.0",
+ "version": "1.10.0",
"description": "Test While Developing (TWD): in-browser test runner with a live sidebar UI. Works with React, Vue, Angular, Solid, Astro, Nuxt, HTMX and vanilla JS, on Vite, Webpack, or a CDN. Built-in API and component mocking.",
"type": "module",
"license": "MIT",
diff --git a/src/constants/version.ts b/src/constants/version.ts
index 95a3aa15..d2f72e44 100644
--- a/src/constants/version.ts
+++ b/src/constants/version.ts
@@ -1 +1 @@
-export const TWD_VERSION = '1.9.0';
+export const TWD_VERSION = '1.10.0';
diff --git a/src/constants/version_cli.js b/src/constants/version_cli.js
index 95a3aa15..d2f72e44 100644
--- a/src/constants/version_cli.js
+++ b/src/constants/version_cli.js
@@ -1 +1 @@
-export const TWD_VERSION = '1.9.0';
+export const TWD_VERSION = '1.10.0';