Repository navigation
Migrate to Vite 8 / ESM TypeScript, add test suite, SSR-safe manual-activation ESM build (v2.0.0) - #27
Open
yellowby wants to merge 53 commits into
Open
Migrate to Vite 8 / ESM TypeScript, add test suite, SSR-safe manual-activation ESM build (v2.0.0)#27yellowby wants to merge 53 commits into
yellowby wants to merge 53 commits into
Conversation
- 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>
Extends the import unit tests: import disabled by config, a single anchor not importing, url and CDN-id requests (uri/ids params, api key, version), the error path releasing anchors to the regular loader, shadow DOM widgets with their render events, import-driven widget cancellation, and the cache being forgotten between cycles. Each test now starts from an unloaded state. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Extends the intersection tests beyond the wrapper guard: the default 1000px margin, margin/threshold mapping to observer options, one observer shared per distinct options, the callback posting intersection entries to the iframe with the options echoed, and unobserve/disconnect on unload-widget and unload. The stub observer records its constructor arguments so the callback can be driven directly. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The lazy iframe loader had no tests: the src built from data-iframely-url (query kept, v/app added), native loading="lazy" when the flag says the browser supports it, the server-side lazy=1 fallback otherwise, the configured theme, the iframe-ready event, and iframes that already carry a src being left alone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
USAGE.md now reflects the server stub exposing constants, the two npm entries coexisting, and autorun being the one key configure() can switch back on. The README links the guide and the changelog from its introduction and shows the configure-based way to restore auto-run for the ES module. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Release notes since 1.11.0: the ESM-only package and exports map, manual activation of the ES module, the configure() rename, dropped legacy browser support, template-literal options templates, the Node 24 toolchain; the new configure/unload APIs, SSR support, feature- detected lazy loading and the test suite; and the fixes landed on this branch. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
yellowby
force-pushed
the
feature/vite-esm
branch
from
October 19, 2026 10:06
d5430b9 to
9bcdf22
Compare
Version 2.0.0-next.1, to be published under the `next` dist-tag so `latest` stays on 1.x. The changelog section is dated, the README explains the tag, and CHANGELOG.md and USAGE.md now ship in the tarball. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The script's two progress lines used console.log, which the project's lint rule reserves for warn/error. Write them to stdout directly, as the script already does for the consumer runs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every bundle includes the singleton module, and since the stub gained the public constants it dragged the constants factory and the no-op stub into the options and i18n bundles too, which never run on a server: options.min.js grew 16% gzipped and the i18n bundles 37-44% compared with 1.11. The singleton is now a bare object on the server and the npm entries (the only ones imported during SSR) apply the stub from server-stub.ts. Gzipped: options.min.js 2864 -> 2236 bytes, options.i18n.de.min.js 2100 -> 1460, options.i18n.fr.min.js 1863 -> 1213, all below their 1.11 sizes; embed.min.js 7503 -> 7483. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Same behavior, less code: classList/parentElement instead of className.split walks, NodeList.forEach and for..of instead of index loops, Array.prototype.some/includes for the list checks, document.head and textContent for the default styles, a hoisted absolute-url regex, and shared helpers for the known-domains list and the iframe selector that unload() and the init sizing pass were each building by hand. parseQueryString keeps its exact output, including for keys without a value and urls ending in a bare '?'. embed.min.js: 21926 -> 21365 bytes raw, 7470 -> 7422 gzipped. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
serve.mjs served the whole repo root on every interface, so anyone on the LAN could fetch /.env with the real IFRAMELY_API_KEY, or /.git. Listen on 127.0.0.1 only and refuse any path with a dot-segment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
unload() fires unload-widget so the intersection observers and the lazy placeholder timeouts let go of the widget, but cancelWidget removed the widget without it: observers kept the detached iframes. Fire it there too, and use remove() so an already detached widget no longer throws. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rolldown named the chunk holding the whole core after its smallest module, so dist/esm/chunks/server-stub.js was 40 KB of browser code. Name the chunk that contains boot-core.ts explicitly; the chunking itself is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both share window.iframely.config, so importing the module before DOMContentLoaded also turns autorun off for an embed.js script tag on the same page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
findIframe put message.src, which any window can post, into an attribute selector as is. A quote or backslash in it made querySelectorAll throw inside the message listener. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
clearWrapperStylesAndClass still walked className.split(' '), as utils
did before dd9103d; the text option's Enter check used the deprecated
keyCode. Same behavior; the wrapper cleanup gets tests pinning it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every form rendered its inputs with id equal to the option key, so two forms on one page had duplicate ids and a label's for could point at the other form's input. Prefix the ids with the form's id; names and everything bound through them are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A value containing '=' (base64 padding, a nested query) was cut at its first '='; a stray '%' made decodeURIComponent throw, which during init aborted the whole boot; a bare key such as ?lazy became the string "undefined" and was sent on to the API as such. Split at the first '=' only, keep undecodable values as written, and give bare keys ''. '+' stays literal on purpose: URLSearchParams would turn it into a space, which breaks raw urls in data-iframely-url. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
translator.ts was the one module still writing to the singleton at import time, so on the server an i18n import put a real, label-holding translator on the shared stub. Install it when labels are registered, which every i18n bundle does on load, and skip that without a window. The stub gets an identity optionsTranslator like its other no-ops. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The package already ships src/, but nothing pointed at it. Emit .js.map for dist/esm and .d.ts.map for dist/types, so debuggers, stack traces and go-to-definition land in the TypeScript sources. The script tag bundles are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bundles feature-detect browser support (native iframe lazy loading among others), so Chromium alone was not enough. All 15 tests pass in all three engines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bump oxlint to 1.89 and vite to 8.3.5. oxfmt 0.58 pins tinypool to 2.1.0, which has a known vulnerability; a pnpm override lifts it to ^2.1.2 (resolves 2.2.0). pnpm audit is clean. pnpm writes pnpm-workspace.yaml with 2-space indents, which oxfmt's tabWidth 4 rejects, so oxfmt ignores that file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
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 indist/esm/and declarations indist/types/.src/rewritten as strict-TypeScript ES modules; side-effectful modules exportboot()so double-loading a bundle still boots exactly once._.escape()without lodash and threw at runtime) and adds HTML-escaping of scalar interpolations.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)..node-versionfor native type stripping of*.config.ts; noenginesfield, so npm consumers are unaffected.Tests & demo workflow
demo/pages double as e2e fixtures and a manual playground served byscripts/serve.mjs(pnpm serve).serve.mjssubstitutes{{IFRAMELY_API_KEY}}at request time from the environment or a git-ignored the key is never written to committed filee inertTESTkey.ESM public API
iframely.configure(1f11c44)DOMContentLoaded; consumers calliframely.load()explicitly (window.iframely = { config: { autorun: true } }restores script-tag behavior). IIFE bundles are unchanged.extendOptionsrenamed toiframely.configure, withextendOptionskept as a deprecated alias.iframely.unload()as the inverse ofload()(7196103)unload(container?)removes library-built widgets (foreign iframes untouched), firingunload-widgetper widget and a finalunloadevent; modules release observers and timers through those events — fixing an IntersectionObserver leak in long-lived SPbuildOptionsFormnow warns and returns instead of throwing when the embed bundle isn't present.Breaking changes
type: module) with an exports map; deep imports of old paths won't resolve.iframely.load()or opt in toautorun.extendOptionsis deprecated in favor ofiframely.configure(alias still works).🤖 Generated with Claude Code