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 7 commits into
mainfrom
feature/vite-esm
Open

Migrate to Vite 8 / ESM TypeScript, add test suite, SSR-safe manual-activation ESM build (v2.0.0)#27
yellowby wants to merge 7 commits into
mainfrom
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 and others added 7 commits July 10, 2026 20:50
- 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>
@yellowby
yellowby requested a review from nleush July 16, 2026 14:43
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