Skip to content

Migrate to Vite 8 / ESM TypeScript, add test suite, SSR-safe manual-activation ESM build (v2.0.0) - #27

Open
yellowby wants to merge 81 commits into
devfrom
feature/vite-esm
Open

yellowby wants to merge 81 commits into
devfrom
feature/vite-esm

Conversation

@yellowby

Copy link
Copy Markdown
Member

Summary

Modernizes embed.js end to end: webpack 5 + EJS is replaced with Vite 8 and strict ESM TypeScript, a full unit + browser test suite is added, and the new ESM build becomes manual-activation and SSR-safe with a richer public API (configure, load/unload). Classic IIFE script bundles keep their filenames and behavior. Version bumps to 2.0.0.

Changes by commit

Build & toolchain

  • Migrate to Vite 8, ESM-only TypeScript, template literals (2a2fc09)
    • Vite 8 (Rolldown) via scripts/build.js: five single-entry IIFE builds emit the classic unhashed bundles (embed.js, embed-options.js, options.js, options.i18n.de/fr.js + .min.js), plus an ESM build in dist/esm/ and declarations in dist/types/.
    • All of src/ rewritten as strict-TypeScript ES modules; side-effectful modules export boot() so double-loading a bundle still boots exactly once.
    • Six EJS templates replaced with template-literal functions — fixes the group template (compiled to _.escape() without lodash and threw at runtime) and adds HTML-escaping of scalar interpolations.
    • Legacy-browser code dropped (ActiveX/Flash, attachEvent, vendor-prefixed rAF, IE fallbacks, Chrome-77 UA sniff); targets Vite's baseline-widely-available.
    • package.json: type: module, exports map (., ./options, ./i18n/de, ./i18n/fr).
  • Migrate toolchain to TypeScript 7, oxlint and oxfmt (a8cc108)
    • TypeScript 5.9 → 7.0 (native compiler) for typecheck and declaration emit.
    • ESLint replaced with oxlint (correctness as errors); oxfmt for formatting (tabs, single quotes) with a one-time reformat.
    • Node pinned to 24.18 via .node-version for native type stripping of *.config.ts; no engines field, so npm consumers are unaffected.

Tests & demo workflow

  • Add test suite: Vitest unit tests and Playwright browser tests (ab5a79f)
    • 72 jsdom unit tests covering utils, events, messaging, theme, options renderer/form-builder, and translator.
    • Playwright e2e specs: script-src config parsing, anchor unfurl, resize/cancel message flows, API surface snapshot, options form + i18n, double-load, ESM singleton identity. The Iframely endpoint is route-stubbed — no network or API key needed.
    • demo/ pages double as e2e fixtures and a manual playground served by scripts/serve.mjs (pnpm serve).
  • Support a real Iframely API key for demo pages via .env (273366d)
    • serve.mjs substitutes {{IFRAMELY_API_KEY}} at request time from the environment or a git-ignored the key is never written to committed filee inert TEST key.

ESM public API

  • Make the ESM build manual-activation, SSR-safe; add iframely.configure (1f11c44)
    • ESM entries no longer auto-run on DOMContentLoaded; consumers call iframely.load() explicitly (window.iframely = { config: { autorun: true } } restores script-tag behavior). IIFE bundles are unchanged.
    • Importing is safe in isomorphic environments (SvelteKit, etc.): the server gets a stateless no-op stub, so a server pass neither throws nor runs side effects.
    • extendOptions renamed to iframely.configure, with extendOptions kept as a deprecated alias.
  • Add iframely.unload() as the inverse of load() (7196103)
    • unload(container?) removes library-built widgets (foreign iframes untouched), firing unload-widget per widget and a final unload event; modules release observers and timers through those events — fixing an IntersectionObserver leak in long-lived SP
    • Known limits (anchor restoration, import/shadow widgets) documented in types and README.
  • Fail silently when options.js is used without embed.js (d5430b9)
    • buildOptionsForm now warns and returns instead of throwing when the embed bundle isn't present.

Breaking changes

  • Package is now ESM-only (type: module) with an exports map; deep imports of old paths won't resolve.
  • ESM entries no longer auto-activate — call iframely.load() or opt in to autorun.
  • Legacy-browser support removed (IE, pre-baseline browsers).
  • extendOptions is deprecated in favor of iframely.configure (alias still works).

🤖 Generated with Claude Code

@yellowby
yellowby requested a review from nleush July 16, 2026 14:43
yellowby and others added 29 commits October 19, 2026 11:47
- Replace webpack 5 + ejs-loader with Vite 8 (Rolldown) driven from
  scripts/build.js: five single-entry IIFE builds emit the classic
  unhashed bundles (embed.js, embed-options.js, options.js,
  options.i18n.de/fr.js + .min.js variants), plus a multi-entry ESM
  build in dist/esm/ and tsc-generated declarations in dist/types/.
- Rewrite all src/ as strict-TypeScript ES modules. Side-effectful
  modules now export boot() called by the entries in the original CJS
  execution order under the _loaded guard, so double-loading bundles
  still boots exactly once despite ESM import hoisting.
- Replace the six EJS templates with template-literal functions in
  options/renderer.ts. Fixes the group template, which compiled to
  _.escape() without lodash bundled and threw at runtime, and adds
  HTML-escaping of scalar interpolations (previously raw).
- Drop legacy-browser support: ActiveX/Flash detection, attachEvent,
  vendor-prefixed rAF/setImmediate, IE styleSheet.cssText, innerText
  fallbacks, and the Chrome-77 UA sniff (lazy-loading-native.js
  removed; SUPPORT_IFRAME_LOADING_ATTR hardcoded true). Build targets
  Vite's baseline-widely-available.
- package.json: type=module, v2.0.0, exports map (., ./options,
  ./i18n/de, ./i18n/fr) pointing at dist/esm + dist/types; webpack and
  ejs devDependencies removed; flat ESLint config with
  typescript-eslint; delete stale dist/embed-autoplay*.
- Add demo/ smoke-test pages used to verify script-src config parsing,
  API surface parity, options form + i18n, double-load, and ESM
  consumption in a real browser.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Unit tests (tests/unit/, jsdom): utils (query-string handling,
  endpoint building, extendOptions normalization, widget wrappers,
  setStyles), events pub/sub, messaging (postMessage origin trimming,
  findIframe incl. shadow roots, message listener + pym fallback),
  theme, options renderer (escaping semantics, all six templates),
  form-generator, form-builder, and translator. 72 tests.
- Browser tests (tests/e2e/, Playwright + chromium): script-src config
  parsing, anchor unfurl to the exact API endpoint, resize and
  cancelWidget message flows, link recovery on cancel, pre-migration
  API surface snapshot, options form + German i18n second-script flow,
  double-load boots once, ESM build singleton identity. The Iframely
  endpoint is route-stubbed, so tests need no network or API key.
- demo/ pages double as the e2e fixtures and a manual playground
  served by the new scripts/serve.mjs (pnpm serve); Playwright starts
  it automatically. Replace stale demo/old.html with
  demo/cancel-recover.html.
- Fix Iframely.setTheme type to include its optional container param.
- Scripts: test, test:watch, test:e2e, serve.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Demo pages use an {{IFRAMELY_API_KEY}} placeholder in script srcs;
  scripts/serve.mjs substitutes it at request time from the
  environment or a git-ignored .env at the repo root, falling back to
  the inert TEST placeholder. The key is only injected into locally
  served responses and never written to committed files.
- Add .env.example; git-ignore .env.
- Pin the Playwright web server to IFRAMELY_API_KEY=TEST and disable
  reuseExistingServer so tests never assert against a dev server that
  is running with a real key.
- Document the workflow in the README.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- TypeScript 5.9 -> 7.0 (native compiler) for typecheck and
  declaration emit; tsgo requires an explicit rootDir in
  tsconfig.build.json for the dist/types layout. The typescript-eslint
  dependency was what pinned TS to 5.x.
- Replace ESLint (+ @eslint/js, globals, typescript-eslint) with
  oxlint: correctness category as errors plus the carried-over
  no-console/no-empty rules, configured in oxlint.config.ts.
- Add oxfmt for formatting, configured in oxfmt.config.ts: tabs
  (width 4), single quotes, no trailing commas; dist/, demo/ and
  markdown excluded. One-time reformat of the codebase to tabs.
- Scripts: lint -> oxlint, new format/format:check; prebuild now runs
  typecheck + lint + format:check.
- Pin Node 24.18 via .node-version: the *.config.ts files rely on
  Node native type stripping, which the previous default Node 22.16
  lacks. No engines field so npm consumers are unaffected.
- Rewrite the events test this-binding assertion via vi.fn mock
  contexts (oxlint no-this-alias).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- ESM entries (main.ts, main-options.ts) no longer auto-run on
  DOMContentLoaded: config.autorun defaults to false, and consumers
  activate embeds explicitly with iframely.load(). Pre-set
  window.iframely = { config: { autorun: true } } before importing to
  restore the script-tag behavior. IIFE script bundles are unchanged.
- Importing is now safe in isomorphic environments (SvelteKit, etc.):
  on the server iframely.ts exports a detached, stateless no-op stub;
  dom-ready and the entry bootstraps skip without browser globals, so
  a server pass neither throws nor runs side effects.
- Rename extendOptions to iframely.configure; extendOptions stays as
  a deprecated alias registered in deprecated.ts and marked
  @deprecated in the types. Internal callers switched.
- Tests: new node-environment SSR suite (imports every public entry,
  asserts inert no-op behavior), new e2e spec proving no auto-run on
  import and activation via load(), surface snapshot gains the
  `configure` key, alias identity pinned in unit tests.
- Document the ESM/isomorphic usage flow in the README.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- unload(container?) removes the widgets the library built (domain-
  matched iframes and pending data-iframely-url lazy iframes with the
  proper wrapper structure; foreign iframes untouched), firing
  unload-widget per widget and a final unload event.
- Modules release their state through those events: intersection
  unobserves removed iframes and drops all observers on a full unload
  (observers held strong refs - the actual leak in long-lived SPAs),
  lazy-img-placeholder clears pending waiting-widget timers, import
  resets transient import state.
- Not covered by design: restoring consumed anchors and import/shadow
  widgets; documented in types and README. SSR stub gains a no-op.
- Tests: unload unit suite, e2e load->unload round trip, surface
  snapshot + SSR no-op coverage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
buildOptionsForm now checks at call time that the embed bundle has
provided iframely.trigger and iframely.addEventListener; without them
it logs a console.warn and returns instead of throwing from
form-builder. Checked per call, not at boot, so loading options.js
before embed.js keeps working. Covered by a unit test booting only the
options module and an e2e spec with an options.js-only fixture page
asserting zero page errors, an empty container, and the warning.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
configure() refuses to override a key whose current value is false so
that script-src params can permanently disable a feature. The ESM
entries write autorun = false at import time, which turned
iframely.configure({ autorun: true }) into a silent no-op and left
pre-setting window.iframely before the import as the only way back to
script-tag behavior. autorun is a mode, not a feature flag: exempt it
from the sticky-false rule.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The exports map declared only "types" and "import", so a CommonJS
require() on Node 22.12+/24 (which supports require(esm)) and any
resolver matching on the "default" condition failed with
ERR_PACKAGE_PATH_NOT_EXPORTED. Point "default" at the same ESM file for
all four entries.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The oxlint/oxfmt TypeScript config files rely on Node's native type
stripping, so older Node fails late in prebuild with a parser error and
no hint of the actual requirement. devEngines (rather than engines)
makes pnpm check this up front for contributors without warning every
consumer who installs the browser library on an older Node.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Vite migration replaced the old Chrome >= 77 check with a constant
true, so browsers without native iframe lazy loading (Safari < 16.4,
Firefox < 121) got loading="lazy" ignored and, because the flag said
native support was present, lost the server-side lazy=1 fallback as
well. Detect the attribute on the iframe prototype at boot.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The server stub provided only no-op methods, while the Iframely type
declares VERSION, DOMAINS, CDN, SUPPORTED_THEMES and the pre-2.0
widgets/events namespaces as always present. Isomorphic code reading any
of them during SSR threw. Move the constants into a createConstants()
factory shared by the browser boot (const.ts) and the stub; a factory so
that the stub, one module instance shared across requests, never aliases
arrays a page could mutate. The boot keeps preserving a CDN set before
load, as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
index-options.ts ran the options boot inside the same `_loaded` guard
that protects the core bootstrap, so whenever a plain embed entry had
evaluated first (embed.js followed by embed-options.js, or the base npm
entry imported before the options entry) the whole block was skipped and
iframely.buildOptionsForm never existed. Move the core bootstrap into
bootCore(), shared by both entries and guarded once, and boot options on
every options entry, as the standalone options.js bundle already did.

The double-load browser test only counted 'init' and therefore passed
on the broken behavior; it now also asserts buildOptionsForm exists.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
exec_body_scripts walked the live childNodes list while removing the
<script> nodes it collected, so the sibling that shifted into the freed
index was skipped: it stayed in the DOM inert (innerHTML-inserted scripts
never execute) and was never recursed into. A widget whose HTML carried
two adjacent scripts lost the second one. Iterate over a snapshot.

Adds the first unit tests for the import flow; the JSONP request is
intercepted before it reaches the document so nothing is fetched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
unload() cleared the import cache but left the in-flight JSONP <script>
in <head>. When it responded (or errored), importReady() re-triggered
'load' on the anchors still in the page and rebuilt widgets in a view
that had just been torn down. Now a full unload removes the pending
script and its error handler, buildImportWidgets ignores a response for
an import that is no longer pending (a removed <script> may still
execute), and the anchors queued for that import lose data-import-uri so
a later load() considers them again instead of skipping them forever.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
When an option's value matched none of its choices, no radio was checked
and the radio getter dereferenced a missing input with a non-null
assertion. Any later change elsewhere in the form threw inside the
change handler, so 'options-changed' never fired again. Report an
unselected group as "no value"; the query builder already drops
undefined keys.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
messaging.ts passes an undefined widget when the posting iframe has no
.iframely-embed/.iframely-responsive wrappers, a layout the resize path
supports. The intersection handler dereferenced widget.iframe without
the guard its sibling handlers use and threw out of the window message
listener. Adds the first intersection tests, with a stubbed observer.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
With embed.js?parent=<class>, cancelWidget removes the host's wrapper
around the library's own wrappers, but unload() only removed the
max-width wrapper and left an empty host element in the layout. Share
cancelWidget's ancestor lookup as utils.findParentByClass and use it in
both places.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The key of each option, which comes straight from the API response, was
concatenated into '[name="<key>"]'. A key containing a quote or a
backslash made querySelectorAll throw a SyntaxError while binding the
form, so the form rendered without any change handlers. The templates
already HTML-escape the same key; escape it for the selector too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The slider template interpolates the option value into
style="--val: ...", and HTML escaping does not neutralize CSS there: a
string value such as "50; background:url(...)" from a
setIframelyEmbedOptions message became a live declaration. The
generator accepted any string for a ranged option. Coerce the value to
a number and fall back to the range minimum when it is not one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The containment check compared the resolved path with startsWith(root)
and no trailing separator, so a request such as /..%2f<sibling>/file
(URL parsing neutralizes a plain /../, an encoded slash survives it)
resolved to a sibling directory of the repo and was served. Compare
against the root as a directory prefix.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
lazy-img-placeholder.ts carried a byte-for-byte copy of the private
helper in utils.ts, so the rule for what counts as a wrapper child could
silently diverge between getIframeWrapper and hidePlaceholderThumbnail.
Export the utils version and use it in both places.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The emitted declarations copy import specifiers verbatim, so the
extensionless relative imports in src/ made every shipped .d.ts fail to
resolve under moduleResolution node16/nodenext (TS2834), which also hid
the re-exported types from such consumers. Explicit .js specifiers are
mapped back to the .ts sources by TypeScript, Vite and Vitest, and emit
declarations that resolve everywhere.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Nothing exercised the package as npm consumers see it: the browser tests
load dist/esm by path and the unit tests import src/, so the exports map
and the shipped declarations were unverified (the missing "default"
condition would never have surfaced, nor the nodenext declaration
failure fixed in the previous commit). scripts/check-package.mjs packs
the tarball, installs it into a throwaway consumer directory, imports
and requires every "exports" entry, asserts the server stub, and
typechecks a strict consumer under both bundler and nodenext resolution
with skipLibCheck off. Run with `pnpm test:pkg`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The containment rule lived inline in the request handler, so the fix
for the sibling-directory bypass had no automated test (a live test
would need a sibling directory sharing the repo's name). Move it into
scripts/serve-path.mjs as resolveUnderRoot() and cover the root itself,
paths below it, decoded-slash traversal to a same-prefix sibling, and
decoded-backslash traversal on Windows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
widgetsCache kept the anchors queued for an import whenever the cycle
ended without a response for their url (request error, or a response
that did not cover them). A later import of the same url then rendered
into the stale, detached elements as well, firing the widget events once
per leftover entry. importReady() now empties the cache along with the
data-import-uri markers it already clears.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
yellowby and others added 30 commits October 26, 2026 11:53
On pushes to main and dev and on pull requests. One job runs the frozen
install (with pnpm's supply-chain checks), pnpm audit, typecheck, lint,
format check, unit tests, the build, a check that the committed dist/
matches the build, and the package check. A second job runs the
Playwright tests in Chromium, Firefox and WebKit and uploads the test
results on failure. Actions are pinned to commit SHAs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adopted from publicly's audit.yml: pnpm audit is compared against the
committed audit.log snapshot on every push, on PRs to dev and main,
weekly on Mondays and on demand. A new advisory fails the run until the
snapshot is reviewed and refreshed with `pnpm audit > audit.log`.
Actions are SHA-pinned like ci.yml; Node comes from .node-version.

CI no longer runs a plain pnpm audit, which would have failed on
advisories already acknowledged in the snapshot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
iframely.VERSION is the API protocol version sent as v= and must stay 1,
so nothing told which build was running. LIB_VERSION is injected from
package.json at build time (and in Vitest) via a define, and is also on
the server stub. Unbuilt sources fall back to a dev placeholder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
If a pre-2.0 embed.js (typically the cloud script API embed codes
append) booted first, bootCore() returned silently and the page later
failed with "iframely.unload is not a function". Taking over the
foreign core is not possible (its listeners and widgets live in its own
closure), so warn once, naming both copies and how to load just one.
Same-major sharing stays silent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
USAGE documented `cdn` as a configure() key, but nothing read
config.cdn, so iframes served from your own CDN were neither resized
nor found by unload(). configure() now normalises the value (protocol
and trailing slash stripped, disabled values ignored) into iframely.CDN
and keeps it out of config. A pre-set config.cdn applies at boot, and a
script src ?cdn= still wins, now through the same path. Message routing
uses knownDomains(), so a CDN assigned with a protocol matches too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
load() only knew the whole document, an <a>, a lazy <iframe> or
(container, url); any other element was ignored, so components had to
rescan the page on every mount while unload(container) was already
scoped. A shared loadRoot() helper now lets the document-scan handlers
take an element, fragment or shadow root: links, lazy iframes, size
requests to existing iframes, and import-mode batching of the
container's links. A document-wide load() now also requests sizes, as
USAGE already promised.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two behaviours integrations rely on were undocumented: unload(el)
works on a container the framework already detached, and since
unload() does not restore consumed anchors, each new API response
needs fresh markup. USAGE gets a "Framework lifecycle" section, and
the Svelte (now an attachment) and React (now keyed on html) recipes
pair load(el) with unload(el). A test locks in the detached case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
on/trigger/triggerAsync, load() and configure() took any arguments, so
consumers got no help with signatures. An IframelyEvents map (payloads
checked against every trigger site) types the callbacks by event name;
the fallback overload only accepts names outside the map, so a known
event with a wrong payload is rejected instead of falling through to
any. load() gets one overload per form, configure() takes
IframelyConfig. The import loader's internal 'load' re-dispatch with
widget data goes through an untyped view. check-package adds typed
consumer calls and @ts-expect-error cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The snippet called iframely.configure() right after an async embed.js
tag. The script has not run at that point, so it threw "iframely.configure
is not a function" in every browser. Pre-setting window.iframely = {
config } before the tag is read at boot regardless of load timing, with
the src query string merged on top. A demo page and an e2e test cover
the pattern.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VERSION read like the library release but is the API protocol version
sent as v=. Next to LIB_VERSION the new name says what it is. The
library now reads API_VERSION only; VERSION stays on the surface with
the same value as a deprecated alias for existing integrations, as
extendOptions did for configure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The warning compared major versions, so an older 2.x core missing a
method a newer 2.x copy provides stayed silent and the page failed with
"... is not a function". It now checks the booted core for every method
this copy installs (CORE_API, kept in sync with the boot by a test) and
names the missing ones; a different major still warns, since the same
names may behave differently. LIB_VERSION names both copies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LIB_VERSION only helps once the script has run. A copy pasted into a
CMS or fetched from the cloud CDN had no name or version at all. All
17 bundles (minified and ESM included) now start with
/*! @iframely/embed.js <version> | MIT | <repo> */, which also carries
the license notice. check-package verifies it on the packed tarball,
and the README tells reporters where to find the version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The package is @iframely/embed.js; @iframely/embedjs does not exist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Version 2.0.0-next.2, for the `next` dist-tag so `latest` stays on 1.x.
The Unreleased changelog section is dated. 2.0.0-next.1 was never
published to npm, so its section no longer says it was, and next.2 is
marked as the first 2.0 release there. dist/ is rebuilt for the new
LIB_VERSION and bundle banners.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The init handler applied a pre-set window.iframely.config.cdn and then
read the script src, where a script at the root of its host
(https://static.example.com/embed.js) makes that host the CDN. The
fallback overwrote the configured CDN, so endpoints went to the script
host and the real CDN's iframes were no longer recognised. The script
host now only applies when no CDN was configured; ?cdn= in the src
still wins. The e2e fixtures serve from /dist/, which CDN_RE does not
match, so only a unit test covers it.
With two or more anchors that can be neither imported nor unfurled
(empty data-iframely-url and no href), makeImportAPICall queued
nothing and triggered 'load' again, whose import handler found the same
anchors: unbounded recursion ending in "Maximum call stack size
exceeded", which also kept the later loaders from running. The
re-trigger was redundant: every anchor has already gone through the
usual build, and the 'load' that started the import pass carries on
with the remaining handlers. The importReady() call next to it had
nothing to clean up either, since no import was started.
When an import ended (failure, a response that omits a url, or a
cancelling unload), the anchors still waiting were found with
document.querySelectorAll. Anchors queued by load(shadowRoot), a
fragment or a detached container are out of its reach: they kept
data-import-uri for good, and both the ahref and the import loader skip
such anchors on every later load. They are now taken from widgetsCache,
which holds every queued anchor; the document query stays as a
supplement for anchors that carry the attribute without being cached.
While an import is pending, both the import and the ahref loader leave
a document-wide load alone. That was fine when only a document load
could start an import, which then covered every anchor. Since
load(container) can start one too, a load() arriving meanwhile (the
DOMContentLoaded autorun, typically) was dropped, and the anchors
outside the container stayed bare links. The import loader now
remembers such a load and replays it when the container import ends.

unload(container) also cancels a pending import that the load of that
container (or of one inside it) started, so a route change no longer
leaves the block in place. The container's anchors are released after
the replay, so the waiting document load does not build them.
With the parent option set, unload() removes the nearest ancestor
carrying that class together with the widget. The search was not
bounded, so for a component whose own root (or an element above it)
has the class, unload(el) removed el or its ancestor, with unrelated
content, and the framework then failed removing a node already gone.
unload(container) now takes a parent wrapper only inside the
container and otherwise removes just the widget; a document-wide
unload() is unchanged.
Any script or extension can post to the window. A message such as
{ type: 'x', context: { id: 1 } } passed the listener's guard and
reached findIframe, where src.replace threw an uncaught TypeError on
every such message. Only a string is used for the src match now, both
in the listener and in findIframe itself; the iframe is still found by
its contentWindow.
A full unload disconnected and dropped every intersection observer.
unload() does not reach into shadow roots, so widgets there stayed on
the page but never received another intersection message. Each
observer now tracks its targets: unload-widget releases a removed
iframe as before, and a full unload releases the iframes that are no
longer in the document and drops only the observers left with nothing
to observe.
main.ts and main-options.ts wrote autorun = false into
window.iframely.config, which every embed.js on the page shares. A
script-tag copy (one a CMS adds, or the 1.x cloud script that API
embed codes include) then stopped auto-running, and a 1.x copy could
not be switched back on: it has no configure(), and its extendOptions
does not overwrite false. That write also happened when this copy did
not even boot.

The default is now per copy, in dom-ready.ts: the npm entries switch
their own off, and an explicit config.autorun still applies to every
copy. With nothing writing false behind the user's back, configure()
no longer needs its special case for the key.
In development StrictMode (the Next.js default) runs an effect, its
cleanup and the effect again on the same element. The recipe's first
load(el) consumed the anchors, unload(el) removed the widgets, and the
second load(el) found nothing, because React does not re-apply an
unchanged dangerouslySetInnerHTML: every embed stayed empty. The
cleanup now puts the markup back after unload(el). A unit test covers
the load, unload, restore, load sequence the recipe relies on.
The dotfile refusal looked for a leading dot only. Windows opens .env
and .git under other names too: an 8.3 short name (ENV~1, GIT~1), a
trailing dot or space, or an alternate data stream. Path segments of
those shapes are refused now.

The server binds to loopback, but a page that points its own hostname
at 127.0.0.1 (DNS rebinding) could still read the demo pages, which
carry the API key. Requests whose Host is not a loopback name get 403.
Version 2.0.0-next.3, for the `next` dist-tag so `latest` stays on 1.x.
The Unreleased changelog section is dated. dist/ is rebuilt for the new
LIB_VERSION and bundle banners.
Listeners could only be added, so a component that subscribed on mount
leaked its callback on every re-mount and had to fan out from one
permanent listener instead.

on() now returns a function that removes that one registration, and
off(event, cb) removes every registration of cb. A removed listener is
never called again: dispatch checks a per-listener flag at call time, so
that holds for a triggerAsync already waiting for its frame and for a
listener removed by an earlier one. The listener arrays are replaced
rather than mutated, so removing during a dispatch no longer makes the
next listener skip.

off joins CORE_API, so the foreign-core warning names it when an older
core owns the page. The server stub's on() returns a no-op.
Version 2.0.0-next.4, for the `next` dist-tag so `latest` stays on 1.x.
The Unreleased changelog section is dated. dist/ is rebuilt for the new
LIB_VERSION and bundle banners.
A page that reserves space for an embed had no reliable signal that the
embed has its real size. heightChanged fires only when the computed
height changes, and a frame later, so an embed that loads at its final
size (a fixed-aspect player) never signals; the alternative was reading
the message protocol.

iframe-sized fires synchronously from widgetResize for every size an
embed reports, once the wrapper styles are set, with the iframe and the
reported media.

heightChanged was the only camelCase event name. It is now
height-changed; the old name is deprecated and still fires. Both are
queued from widgetResize, the old one second, so its listeners run after
those of the new name.
Version 2.0.0-next.5, for the `next` dist-tag so `latest` stays on 1.x.
The Unreleased changelog section is dated. dist/ is rebuilt for the new
LIB_VERSION and bundle banners.
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.

1 participant