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

yellowby wants to merge 53 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 5 commits October 19, 2026 11:58
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
yellowby changed the base branch from main to dev October 19, 2026 10:06
yellowby and others added 19 commits October 20, 2026 10:37
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>
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