Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
426e15d
Migrate to Vite 8, ESM-only TypeScript, template literals
yellowby Jul 10, 2026
4eab442
Add test suite: Vitest unit tests and Playwright browser tests
yellowby Jul 10, 2026
e8e5814
Support a real Iframely API key for demo pages via .env
yellowby Jul 10, 2026
f8f82ef
Migrate toolchain to TypeScript 7, oxlint and oxfmt
yellowby Jul 11, 2026
e307711
Make the ESM build manual-activation, SSR-safe; add iframely.configure
yellowby Jul 11, 2026
46d5711
Add iframely.unload() as the inverse of load()
yellowby Jul 11, 2026
ad0837e
Fail silently when options.js is used without embed.js
yellowby Jul 11, 2026
d01d62e
update package.json
yellowby Oct 19, 2026
878e8cc
update deps
yellowby Oct 19, 2026
1468951
Let configure() re-enable autorun after the ESM entries disable it
yellowby Oct 19, 2026
2134487
Add a default condition to every package export
yellowby Oct 19, 2026
3c46209
Declare the Node 24 requirement for development via devEngines
yellowby Oct 19, 2026
d580a0f
Feature-detect <iframe loading="lazy"> instead of assuming support
yellowby Oct 19, 2026
0e1f51e
Expose the public constants and legacy namespaces on the SSR stub
yellowby Oct 19, 2026
76099c0
add usage guide
yellowby Oct 19, 2026
7320147
Boot the options subsystem outside the shared core guard
yellowby Oct 19, 2026
0160918
Run every script of an imported widget
yellowby Oct 19, 2026
c0adb65
Cancel a pending import on unload and ignore its late response
yellowby Oct 19, 2026
f35a6e6
Tolerate a radio group with no selection in the options form
yellowby Oct 19, 2026
a42bad4
Ignore send-intersections from iframes without the wrapper structure
yellowby Oct 19, 2026
84b589e
Make unload() remove the configured parent wrapper like cancelWidget
yellowby Oct 19, 2026
1bf97f3
Escape option keys in the form builder's attribute selector
yellowby Oct 19, 2026
297c117
Only let a number into the range slider's style attribute
yellowby Oct 19, 2026
c4aeec4
Reject paths outside the repo root in the dev server
yellowby Oct 19, 2026
9c9d7f2
Share nonTextChildCount instead of keeping two copies
yellowby Oct 19, 2026
0330fa7
Use explicit .js extensions in relative imports
yellowby Oct 19, 2026
61328a5
Add a packaging check: pack, resolve every export, typecheck a consumer
yellowby Oct 19, 2026
692c857
Unit-test the dev server's path containment
yellowby Oct 19, 2026
1ffee5d
Clear the import cache when an import cycle ends
yellowby Oct 19, 2026
2415dab
Cover import request shaping, widget rendering and cache lifetime
yellowby Oct 19, 2026
477e3d7
Cover intersection observer options, callbacks and teardown
yellowby Oct 19, 2026
97ce746
Add lazy iframe tests
yellowby Oct 19, 2026
b0e2389
Refresh the usage guide for 2.0 and link it from the README
yellowby Oct 19, 2026
9bcdf22
Add the 2.0.0 changelog
yellowby Oct 19, 2026
6dcfbe2
Prepare the 2.0.0-next.1 pre-release
yellowby Oct 20, 2026
da86fe3
Silence the no-console warnings in the packaging check
yellowby Oct 20, 2026
e48786e
Keep the server stub out of the script-tag bundles
yellowby Oct 20, 2026
dd9103d
Modernize utils with DOM and array APIs the build already targets
yellowby Oct 20, 2026
0645416
Keep the dev server's .env and .git off the network
yellowby Oct 26, 2026
5d7db06
Release per-widget state when a widget is cancelled
yellowby Oct 26, 2026
5c97fc2
Name the shared ESM core chunk core.js
yellowby Oct 26, 2026
3047501
Document how the ESM entry's autorun default affects a script tag
yellowby Oct 26, 2026
94dc452
Escape a messaged src before using it in a selector
yellowby Oct 26, 2026
7f48cab
Use classList in the import wrapper cleanup and key in the form builder
yellowby Oct 26, 2026
41e0de6
Scope options form element ids to the form
yellowby Oct 26, 2026
87c871e
Make parseQueryString keep '=' in values and survive bad encoding
yellowby Oct 26, 2026
fda3aa4
Install optionsTranslator from registerLabels, not at import time
yellowby Oct 26, 2026
b6e88ee
Ship source and declaration maps for the npm build
yellowby Oct 26, 2026
8cbd5a5
Run the browser tests in Firefox and WebKit too
yellowby Oct 26, 2026
6ece211
Add the unreleased changelog entries since 2.0.0-next.1
yellowby Oct 26, 2026
3a6510b
Update dev dependencies and patch tinypool via an override
yellowby Oct 26, 2026
83b6a99
Add a GitHub Actions CI workflow
yellowby Oct 26, 2026
2a93b66
Add the audit workflow from publicly; drop pnpm audit from CI
yellowby Oct 26, 2026
c804433
Expose the library release as iframely.LIB_VERSION
yellowby Nov 6, 2026
0cb150a
Warn when another major version owns window.iframely
yellowby Nov 6, 2026
5e2abb9
Make configure({ cdn }) set the iframe host
yellowby Nov 6, 2026
6407bfe
Let load(container) build the embeds inside one container
yellowby Nov 6, 2026
f0d8da6
Document the framework lifecycle for load() and unload()
yellowby Nov 6, 2026
58601ad
Type the events, load() overloads and configure() options
yellowby Nov 6, 2026
9fd919a
Pre-set the config for an async script tag in USAGE §1
yellowby Nov 8, 2026
2ab6437
Rename iframely.VERSION to API_VERSION, keeping VERSION as an alias
yellowby Nov 8, 2026
2f0249c
Warn about a foreign core by its missing methods, not only its major
yellowby Nov 8, 2026
a72d995
Start every bundle with a name, version and license banner
yellowby Nov 8, 2026
2238e1d
Fix the package name in the README update command
yellowby Nov 8, 2026
85144fc
Prepare the 2.0.0-next.2 pre-release
yellowby Nov 8, 2026
6af567e
update deps
yellowby Nov 9, 2026
024f5b4
Keep a pre-set config.cdn when embed.js is served from a host root
yellowby Nov 9, 2026
7db9217
Stop the import loader from re-triggering load when nothing was queued
yellowby Nov 9, 2026
5e6da82
Release queued import anchors from the cache, not a document query
yellowby Nov 9, 2026
352d8f8
Replay a document load() that arrives during a container import
yellowby Nov 9, 2026
565ecac
Bound unload(container)'s parent wrapper removal to the container
yellowby Nov 9, 2026
0f625f2
Ignore a non-string src or context in posted messages
yellowby Nov 9, 2026
470f642
Keep observing the iframes a full unload() leaves on the page
yellowby Nov 9, 2026
965cebf
Keep the npm entries' autorun default out of the shared config
yellowby Nov 9, 2026
6f7f4b2
Make the USAGE React recipe work under StrictMode
yellowby Nov 9, 2026
4a83413
Close the dev server's dotfile and Host header gaps
yellowby Nov 9, 2026
265411c
Prepare the 2.0.0-next.3 pre-release
yellowby Nov 9, 2026
0cbdc8a
Add iframely.off() and make on() return an unsubscribe function
yellowby Nov 10, 2026
255b601
Prepare the 2.0.0-next.4 pre-release
yellowby Nov 10, 2026
28eeffa
Add the iframe-sized event and rename heightChanged to height-changed
yellowby Nov 11, 2026
877a687
Prepare the 2.0.0-next.5 pre-release
yellowby Nov 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Copy to .env (git-ignored) to use a real Iframely API key with the demo
# pages served by `pnpm serve`. scripts/serve.mjs substitutes it into the
# demo HTML at request time; without it the pages use the TEST placeholder.
IFRAMELY_API_KEY=
51 changes: 0 additions & 51 deletions .eslintrc

This file was deleted.

47 changes: 47 additions & 0 deletions .github/workflows/audit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Audit

# Compares pnpm audit against the committed audit.log snapshot, so a new
# advisory fails the run until someone reviews it and refreshes the
# snapshot. Every branch audits its own lockfile: on push, and on PRs
# against the merge result. The schedule matters too: advisories change
# over time without any code changes. GitHub only runs schedules on the
# default branch, so a stale snapshot there fails that run, not your
# branch's.
on:
workflow_dispatch:
push:
branches: ['**']
pull_request:
branches: [dev, main]
schedule:
- cron: '0 6 * * 1' # Mondays 06:00 UTC

permissions:
contents: read

jobs:
audit:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

# Keep in sync with the local pnpm version (and ci.yml). Audit
# works off pnpm-lock.yaml, no install needed.
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
with:
version: 12.14.0

- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .node-version

- name: Verify audit.log matches pnpm audit
run: |
pnpm audit > audit-current.log || true
if ! diff -u --strip-trailing-cr audit.log audit-current.log; then
echo "::error::audit.log is stale. The diff above shows what changed. Review the advisories, then refresh the snapshot with: pnpm audit > audit.log"
exit 1
fi
89 changes: 89 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
name: CI

on:
push:
branches: [main, dev]
pull_request:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

# Actions are pinned to commit SHAs; the comment names the release.

jobs:
check:
name: Typecheck, lint, unit tests, build
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

# Keep in sync with the local pnpm version.
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
with:
version: 12.14.0

- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .node-version
cache: pnpm

# Also enforces the lockfile's supply-chain policies.
- run: pnpm install --frozen-lockfile

# Vulnerabilities: see audit.yml.
- run: pnpm typecheck
- run: pnpm lint
- run: pnpm format:check
- run: pnpm test

- run: pnpm build

# dist/ is committed: it must be exactly what the sources build to.
- name: Check that the committed dist/ is up to date
run: |
if [ -n "$(git status --porcelain -- dist)" ]; then
git status --short -- dist
echo "::error::dist/ is out of date. Run 'pnpm build' and commit the result."
exit 1
fi

- run: pnpm test:pkg

e2e:
name: Browser tests
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
with:
version: 12.14.0

- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .node-version
cache: pnpm

- run: pnpm install --frozen-lockfile

- run: pnpm exec playwright install --with-deps chromium firefox webkit

- run: pnpm test:e2e

- if: failure()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: playwright-test-results
path: test-results/
retention-days: 7
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@
node_modules/
package-lock.json
.npmrc
test-results/
playwright-report/
.env
1 change: 1 addition & 0 deletions .node-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24.18.0
190 changes: 190 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
# Changelog

## 2.0.0-next.5 (2026-11-11)

Fifth pre-release of 2.0, under the `next` dist-tag (`npm install @iframely/embed.js@next`); `latest` stays on 1.x.
The `iframe-sized` event and the `height-changed` event name, on top of 2.0.0-next.4.

### Added

- An `iframe-sized` event, `iframely.on('iframe-sized', (iframe, media) => ...)`, fires each time an embed reports
its size and the library has applied it. `height-changed` stays silent when the reported size equals the current
one and arrives a frame later, so a page could not tell when a new embed had its real size without reading the
`message` protocol itself.

### Changed

- The `heightChanged` event is now `height-changed`, in line with the other event names (`options-changed`,
`iframe-ready`). `heightChanged` is deprecated and still fires, right after `height-changed` and with the same
arguments, so existing listeners keep working.

## 2.0.0-next.4 (2026-11-10)

Fourth pre-release of 2.0, under the `next` dist-tag (`npm install @iframely/embed.js@next`); `latest` stays on 1.x.
Removable event listeners, on top of 2.0.0-next.3.

### Added

- `iframely.off(event, cb)` removes every subscription of `cb` to an event, and `iframely.on(event, cb)` now returns
a function that removes that one subscription. A component can thus drop its listeners when it unmounts; they used
to stay for the life of the page. A removed listener is not called any more, not even by a `triggerAsync` that was
already waiting for its animation frame.

## 2.0.0-next.3 (2026-11-09)

Third pre-release of 2.0, under the `next` dist-tag (`npm install @iframely/embed.js@next`); `latest` stays on 1.x.
Fixes from a review of the 2.0 code, on top of 2.0.0-next.2.

### Changed

- The npm module keeps its "no autorun" default to itself instead of writing `autorun: false` into the shared
`window.iframely.config`. Another `embed.js` on the page (a script tag a CMS adds, or the 1.x cloud script that API
embed codes include) therefore keeps scanning the page on `DOMContentLoaded`; it used to be switched off by the
import, and a 1.x copy could not be switched back on. `configure()` consequently treats `autorun` like any other
key: `configure({ autorun: true })` still turns it on, but not after it was explicitly set to `false`.

### Fixed

- A pre-set `window.iframely = { config: { cdn } }` is no longer replaced by the script's own host when `embed.js` is
served from the root of a host (`https://static.example.com/embed.js`). A `?cdn=` in the script `src` still wins.
- A document-wide `load()` (the `DOMContentLoaded` autorun included) that arrives while the import request of a
`load(container)` is pending is replayed when that import ends; it used to be dropped, leaving the links outside the
container unbuilt. `unload(container)` now cancels a pending import that the container's own `load()` started.
- Links queued for an import from a shadow root, a document fragment or a detached container are released when the
import fails, does not cover them or is cancelled. They used to keep `data-import-uri`, and every later `load()`
skipped them.
- `unload(container)` with the `parent` option removes a parent wrapper only inside the container, never the
container itself or an element above it.
- Two or more `<a data-iframely-url>` links with neither a URL nor an `href` no longer end `load()` in
"Maximum call stack size exceeded".
- A `postMessage` whose `src` or `context` is not a string no longer makes the message listener throw.
- `unload()` keeps the intersection observers of the iframes it leaves on the page (widgets inside shadow roots),
which used to stop receiving intersection updates.
- USAGE.md: the React recipe rendered nothing under StrictMode (the Next.js default in development), where the
effect runs, is cleaned up and runs again on the same element. Its cleanup now restores the markup after
`unload(el)`.
- The development server also refuses the other names Windows accepts for a dotfile (8.3 short names such as
`ENV~1`, a trailing dot or space, `name:stream`), and answers only requests whose `Host` is a loopback name, so a
DNS-rebinding page cannot read the demo pages that carry the API key.

## 2.0.0-next.2 (2026-11-08)

Second pre-release of 2.0, under the `next` dist-tag (`npm install @iframely/embed.js@next`); `latest` stays on 1.x.
2.0.0-next.1 was tagged in this changelog but never published to npm, so this is the first 2.0 release there, and the
changes below come on top of those listed for 2.0.0-next.1.

### Added

- `iframely.LIB_VERSION`: the embed.js release that booted the page, e.g. `'2.0.0'`, injected from `package.json` at
build time and also readable on the server stub. It is not the API protocol version sent as `v=`, see `API_VERSION`
below.
- Every bundle in `dist/`, minified ones included, starts with `/*! @iframely/embed.js <version> | MIT | ... */`, so
a copy (for example in a CMS, or fetched from the cloud) can be identified without running it.
- A console warning when another copy of embed.js initialised `window.iframely` first and its core lacks methods this
copy provides (typically the 1.x cloud script that API embed codes include, which has no `unload()` or
`configure()`; or an older release missing a newer method), or reports a different major version. This copy then
does not start; the warning names both copies, the missing methods and how to load a single copy. Copies that share
a complete core (`embed.js` with `embed-options.js`, or the cloud script with the npm module) stay silent, as
before.
- `iframely.load(container)` builds only the embeds inside an element, document fragment or shadow root: links,
lazy iframes, and a size request to iframes already there; in import mode the container's links are batched into one
request. It pairs with `unload(container)`. Previously any element other than an `<a>` or `<iframe>` was ignored.
`load()` without arguments now also asks existing iframes for their size, as USAGE.md already stated.
- USAGE.md "Framework lifecycle": per-container `load(el)` / `unload(el)`, `unload()` on an already detached container,
and keying the container on the embed HTML. The Svelte recipe is now an attachment, the React one keyed on `html`.
- TypeScript: an `IframelyEvents` map types the callback arguments of `iframely.on` (and `trigger`/`triggerAsync`)
by event name, custom event names stay untyped; `load()` has overloads for `()`, `(container | anchor)` and
`(container, url)`.

### Changed

- Options form element ids (and the labels' `for`) are prefixed with the form id passed to `buildOptionsForm`, e.g.
`w1-_hide` instead of `_hide`, so several forms can share a page. Input names are unchanged.
- Query strings (script `src`, `data-iframely-url`): a value keeps everything after the first `=`, a key without a
value reads as `''` instead of `"undefined"`, and a malformed percent-encoding is kept as written instead of
aborting the boot.
- `iframely.optionsTranslator` is installed when an i18n bundle registers its labels rather than at import time. On the
server it is an identity no-op on the stub.
- The ESM core chunk is `dist/esm/chunks/core.js` (was `server-stub.js`; not a public path). The npm build now ships
source maps and declaration maps.
- TypeScript: `configure()` (and `extendOptions()`) take `IframelyConfig` instead of `Record<string, any>`, so known
keys such as `theme` or `cdn` are checked and other keys must be scalars, as the runtime expects.
- `iframely.VERSION` is renamed to `iframely.API_VERSION`, which says what it is: the API protocol version the library
sends as `v=` (still `1`), not a release number. `VERSION` remains as a deprecated alias with the same value, but the
library no longer reads it, so assigning it does not change `v=`.

### Fixed

- USAGE.md §1 configured a script-tag page by calling `iframely.configure()` right after an `async` script tag, which
throws because the script has not run yet. It now pre-sets `window.iframely = { config: { ... } }` before the
tag, and an e2e test covers that pattern.
- `iframely.configure({ cdn })` sets the iframe host (`iframely.CDN`). The key was documented but did nothing, so
iframes from your own CDN were neither resized nor recognised by `unload()`. The value may carry a protocol or a
trailing slash, and a pre-set `window.iframely = { config: { cdn } }` applies at boot; a script `src` `?cdn=` still
wins. A `CDN` assigned with a protocol is now also matched when routing iframe messages.
- `cancelWidget` fires `unload-widget`, so a cancelled widget's intersection observer and timers are released, and no
longer throws for a widget that is already detached.
- A `postMessage` whose `src` contains quotes or backslashes no longer makes the message listener throw.
- The development server listens on loopback only and refuses dotfiles, so `.env` and `.git` are not served.

## 2.0.0-next.1 (2026-10-20)

First pre-release of 2.0, prepared for the `next` dist-tag but never published to npm; its changes first ship in
2.0.0-next.2. `latest` stays on 1.x until 2.0.0 ships. Changes since 1.11.0: a rewrite of the toolchain and sources with the same classic script
bundles, plus a real ES module build for npm consumers. See [USAGE.md](USAGE.md) for the recipes and the [README](README.md) for self-hosting and builds.

### Breaking changes

- **The npm package is ESM-only.** `package.json` has `"type": "module"` and an exports map with four entries:
`@iframely/embed.js`, `@iframely/embed.js/options`, `@iframely/embed.js/i18n/de` and `@iframely/embed.js/i18n/fr`.
Deep imports of files under `dist/` are not part of the public API. TypeScript declarations are included.
- **The ES module does not auto-run.** Importing `@iframely/embed.js` no longer scans the page on `DOMContentLoaded`.
Call `iframely.load()` when your markup is in the DOM, or call `iframely.configure({ autorun: true })` right after
the import to restore the script-tag behavior. The classic script tags (`dist/embed.js` and friends) are unchanged
and still auto-run.
- **`iframely.extendOptions` is renamed to `iframely.configure`.** The old name remains as a deprecated alias.
- **Legacy browser support is dropped.** ActiveX/Flash detection, `attachEvent`, vendor-prefixed animation frame
shims and the IE stylesheet fallbacks are gone, and the bundles target Vite's "baseline widely available" set.
- **Options form templates are TypeScript template literals** in `src/options/renderer.ts`; the EJS templates and
the webpack `ejs-loader` are gone, so custom `.ejs` templates are no longer picked up. Every scalar interpolation is
now HTML-escaped.
- **Development requires Node 24 and pnpm** (declared via `devEngines`; consumers of the npm package are not
affected). The build is Vite 8 with TypeScript 7, oxlint and oxfmt; webpack, ejs-loader and ESLint are removed.

### Added

- `iframely.configure(options)` to merge configuration from code. `autorun` can be switched on through it.
- `iframely.unload(container?)`, the inverse of `load()`: removes the widgets the library built from the document or a
container, releases their intersection observers and pending timers, cancels a pending import request, and fires
`unload-widget` per widget and a final `unload` event. It does not restore the original `<a data-iframely-url>`
anchors and does not cover import/shadow DOM widgets.
- Server-side rendering support: importing any entry without a `window` yields an inert stub whose methods are no-ops
and whose public constants (`VERSION`, `DOMAINS`, `CDN`, `SUPPORTED_THEMES`, ...) and legacy namespaces are readable.
- `iframely.SUPPORT_IFRAME_LOADING_ATTR` is feature-detected from the iframe prototype instead of being derived from a
Chrome version sniff; browsers without native lazy loading fall back to the server-side `lazy=1` mode.
- A test suite: Vitest unit tests over `src/`, Playwright browser tests against the built bundles with the Iframely API
stubbed, and `pnpm test:pkg`, which packs the npm tarball and checks every export via `import()` and `require()`, the
SSR stub and the type declarations under both `bundler` and `nodenext` resolution.
- Demo pages under `demo/`, served by `pnpm serve`, with an optional real API key from a git-ignored `.env`.
- `USAGE.md`, a short recipe guide for the script tag, bundlers, SSR frameworks and the options form.

### Fixed

- Options form: the group template compiled to a lodash `_.escape` call that was not bundled and threw at runtime.
- Options form: option keys containing quotes or brackets no longer break event binding, a radio group with no matching
value no longer throws on later changes, and the range slider only accepts a number for its `style` value.
- `options.js` loaded without `embed.js` now warns and returns instead of throwing.
- The options subsystem now boots when `embed.js` is loaded before `embed-options.js`, or the base npm entry is
imported before the options entry.
- Import widgets: every `<script>` in a widget runs (a script following a removed script was skipped), a late import
response after `unload()` is ignored, and the import cache is cleared between import cycles.
- The `send-intersections` message from an iframe without the library's wrapper structure no longer throws.
- `unload()` removes the host wrapper configured with `parent=`, like `cancelWidget` does.
- Package exports resolve for `require()` on Node 22.12+ and for resolvers using the `default` condition, and the
shipped declarations resolve under `moduleResolution: node16 / nodenext`.
- The development server rejects encoded-traversal requests that would resolve outside the repository.

### Removed

- The stale `dist/embed-autoplay*` bundles, `lazy-loading-native.js`, the webpack configuration, the EJS templates and
the ESLint setup.
Loading
Loading