fix(frontend): harden module lifecycle, dialogs, and build tooling - #4
Open
corneliusio wants to merge 14 commits into
Open
fix(frontend): harden module lifecycle, dialogs, and build tooling#4corneliusio wants to merge 14 commits into
corneliusio wants to merge 14 commits into
Conversation
Triggers were set to type="submit" (an inverted typo for "button"), so a lightbox button inside any form opened the lightbox and submitted the form. Also only set the type on actual button elements.
Start and end heights were written in the same synchronous block, so the browser only ever saw auto -> 0 / auto -> Npx, which is not interpolable and made slideUp/slideDown jump instantly. Force a reflow between the writes to give the transition an interpolable origin.
- Only fall back to the Vite dev server in the dev environment; a missing manifest in staging/production now fails loudly instead of serving every asset from the dev server port. - Refuse to run the pre-build rmSync when VITE_BASE/VITE_TEMP resolve to an empty path, which would have emptied the webroot. - Pass TINYPNG_KEY into the tinify plugin from the validated env parse; loadEnv() never populates process.env, so the plugin would have thrown on the first raster image in a bundle. - Hash raw asset bytes for the tinify cache checksum instead of a lossy UTF-8 decode, which could collide across distinct images and serve the wrong cached file. Existing cache entries re-tinify once.
The strict AppSchema parse ran at module init, so any mismatch in the Twig-emitted $app object threw while importing nearly every module and killed all page JS. The parse (still strict) now runs on first property access behind a memoized proxy, scoping a failure to the code that actually reads craft, and reports the offending paths via prettifyError.
The dialog was opened via the open attribute, so there was no top layer, focus trap, or inert background, and the Tab handler navigated images without preventDefault, walking focus through the obscured page. Use showModal()/close() for native modal semantics, move the nav buttons (now labeled) inside the dialog, navigate with arrow keys only, restore focus to the trigger on close, and style the native ::backdrop with a defined color in place of the undefined brand token. jsdom ships HTMLDialogElement without its methods, so add a minimal test-only polyfill via a vitest setup file.
The focus trap cached its focusable list at mount (missing async children) and its selector omitted iframes and media controls, so a video modal looped Tab on the close button forever. Query focusables at keydown time with a broadened selector, gate key handling on the active modal id, recapture focus that escapes the dialog, fire onclose when another modal replaces this one, restore focus to the previously focused element on close, and add a label prop for aria-label. Also replace undefined brand-* color utilities with neutral built-ins in Modal and Video.
Sources with null or partial transform args rendered width="0" height="0" images and baked zeroed dimensions into the 2x srcset URL. Fall back to the asset intrinsic size for the attributes and only double dimensions that were actually provided, matching GeneralExtension::imageAttributes().
Desktop and mobile menus each lazy-loaded navigation.main and every dropdown item lazy-loaded item.links, costing roughly 2 + 2N queries per uncached request. Compute the menu once with eagerly(), pass it to the mobile include explicitly, and batch child links.
Non-matching environments received the markup wrapped in a template element, so dev-only block comments (type handles and element ids) shipped in production HTML.
Add a bash shebang (the script uses bash-only syntax but ran under sh via the POSIX fallback, failing on Debian-family systems) and set -euo pipefail so a signed-out 1Password CLI aborts instead of silently writing an empty Tinify key. Quote substitutions and stop clobbering an existing .env on re-run.
pnpm falls back to a project-local store when running inside the ddev container, since it cannot hard-link across the project mount boundary.
init discarded the cleanup functions modules return, so re-running init over the same scope (e.g. after a Sprig/htmx swap) double-bound everything: a second lightbox dialog and keydown listener, self- canceling play/pause clicks, stacked parallax scroll handlers. Track cleanups per scope and selector in a WeakMap, run them before re-binding, and tear down bindings whose elements are gone. Cleanups only run on re-initialization of the same scope; fragment scopes that are discarded without a further init call still leak their bindings until an explicit destroy() counterpart exists.
Greptile SummaryThe PR hardens frontend module lifecycle handling, native-dialog behavior, media rendering, navigation loading, environment configuration, and build safeguards.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains. Important Files Changed
|
A re-initialization that found no matching elements ran its teardown synchronously, so an earlier pass whose dynamic import was still in flight would land afterward and bind its captured, now-detached elements anyway, installing global artifacts with no owner. Track a per-scope, per-selector pass counter and have the async continuation bail when it has been superseded, so only the latest pass ever binds.
Contributor
Author
corneliusio
added a commit
to mostlyserious/craftcms-sveltekit
that referenced
this pull request
Aug 12, 2026
fix: port applicable fixes from mostlyserious/craftcms#4
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
Testing
Note
Harden module lifecycle, dialog focus trapping, and build tooling
WeakMap, cancels stale pending loads, and tears down modules when their elements disappear — re-initialization replaces bindings instead of stacking them.aria-labelvia a newlabelprop.<dialog>element withshowModal()/close(), closing on backdrop click orcancelevent and restoring focus to the trigger on close.node_modules/.vite/tinify/to skip redundant optimizations.craftin global.ts now validateswindow.$applazily on first property access rather than at module initialization;config/vite.phprestricts the Vite dev server to thedevenvironment only.Macroscope summarized 2058b7b.