admin_ui blank dashboard after Vite 8 upgrade (2026-06-19)
Summary
- All merchants' Shopify Marqo dashboards rendered blank in the embedded iframe after the 2026-06-19 01:34 UTC prod deploy.
- The dashboard's chrome (left nav, "Marqo" breadcrumb title, the iframe element itself) appeared, but the iframe's content area was empty.
- Root cause: PR #3651 (Dependabot, merged 2026-06-18T12:46 UTC) bumped Vite from
^5.4.8to^8.0.16. Vite 7 switched its internal globber frommicromatchtotinyglobby, which silently does not match extglob negation patterns like!(*.test.[jt]sx). Theimport.meta.globcall incomponents/shopify/admin_ui/src/App.tsxused such a pattern; on Vite 8 it returned{}, leaving the React Router routes table empty. - Fix: rewrite the glob to match all
.tsx/.jsxfiles insrc/pages/and filter out*.test.tsx/*.test.jsxin JS instead of relying on extglob. Works on both Vite 5 and Vite 8.
Timeline (UTC)
- 2026-06-18T12:46 — PR #3651 merged to
main(Dependabot bump:vite ^5.4.8→^8.0.16, plusvitest,@vitejs/plugin-react,@vitest/coverage-v8, esbuild). CI checks all green (build compiled, unit tests passed). No e2e tests run on this PR.e2e-testsande2e-ui-testsreported asSKIPPED. - 2026-06-18T15:20 —
Deploy Ecommerce Platform Pipelineforsha=1873b818(which contains the dep bump) starts onmain. Staged rollout (dev → staging → preprod → prod) with approval gates. - 2026-06-19T01:34 — Prod-stage
BucketDeploymentuploads the newindex.html,index-CmKfZYbo.js(393.7 KB), andindex-C77IbKlF.csstos3://prod-shopify-app-assets/. Previous bundle wasindex-CbQVTN_t.js(576.8 KB from 2026-06-11). The size dropped 31 % because allsrc/pages/**modules dead-code-eliminated themselves out of the entry bundle (no longer reached viaimport.meta.glob). - 2026-06-19T01:51 — Deploy pipeline reports
success. - 2026-06-19 morning — Merchants report blank Marqo dashboards on
https://admin.shopify.com/store/<shop>/apps/marqo-<shop>.
What was verified
App Bridge IS working
In a synthetic parent test (a <iframe src="https://admin.ecom.marqo.ai/?embedded=1&shop=msqc.myshopify.com&host=..."> hosted on localhost:8765) we captured 18 outbound postMessage calls from the iframe, including:
APP::CLIENT::INITIALIZE(dispatch + subscribe)APP::CLIENT::RPCsubscribeAPP::NAVIGATION::HISTORY::REPLACE(router pushes/)APP::TITLEBAR::BUTTONS::BUTTON::CLICKsubscribeAPP::TITLEBAR::BREADCRUMBS::BUTTON::CLICKsubscribeAPP::MARKETING_EXTERNAL_ACTIVITY_TOP_BAR::BUTTONS::BUTTON::CLICKsubscribeAPP::LOADING::STOP— the app explicitly tells Shopify "I'm done loading"
Same set of messages observed when reloading the iframe inside the real Shopify admin shell at admin.shopify.com. So:
- The bundle executes.
- React mounts.
@shopify/app-bridge-reactinitializes.- The NavMenu
<a>declarations reach Shopify (breadcrumb "Marqo" renders in the outer chrome). - The app signals load-complete.
Where the bundle silently failed
src/App.tsx:15 (before the fix):
const pages = import.meta.glob<PageModule>(
"./pages/**/!(*.test.[jt]sx)*.([jt]sx)",
{ eager: true },
);
Routes.tsx then turned pages into a React Router route table:
const routes = Object.keys(pages).map(...);
On Vite 8, pages === {} because tinyglobby doesn't honor the extglob negation. So routes === [], and <ReactRouterRoutes> mounted with zero children. React Router rendered nothing for /, hence the blank content area. The NavMenu / breadcrumbs / App Bridge handshake still worked because they're declared inline in App.tsx and don't depend on the pages glob.
Bundle string-presence check confirmed the page modules dropped out of the build entirely:
| String | Broken prod (Vite 8) | June 11 working (Vite 5) | After fix (Vite 8) |
|---|---|---|---|
NotFound | 0 | 2 | 2 |
ExitIframe | 0 | 1 | 1 |
Bundle sizes:
| Bytes (gz) | Notes | |
|---|---|---|
| 2026-06-11 working | 576,764 | Vite 5, micromatch — extglob worked |
| 2026-06-19 broken | 393,700 (-31 %) | Vite 8, tinyglobby — pages dead-stripped |
| After fix | 563,512 (+43 %) | Vite 8, simple glob + JS filter — pages restored |
Why no console error, no test failure
- React Router rendering an empty
<Routes>is valid — not a crash. - The bundle's
npm run buildstep succeeded (TypeScript compiled, Vite emitted output). npm testcoverssrc/*unit tests; none of them exerciseApp.tsx's page-glob result.- No e2e test loads the embedded iframe and asserts that any page route mounts.
- Dependabot CI for this PR ran only the green checks above.
e2e-testsande2e-ui-testsreportedSKIPPED.
That's the gap: a passing vite build plus passing unit tests do not detect "the entry bundle no longer contains any of the page modules."
Mechanism of the postMessage console warning
While the iframe is broken-blank, console occasionally shows:
Failed to execute 'postMessage' on 'DOMWindow': The target origin provided ('https://admin.ecom.marqo.ai') does not match the recipient window's origin ('https://admin.shopify.com').
This is from Shopify's outer admin code (common-42985c2a6792.js) targeting admin.ecom.marqo.ai but landing on a window that's at admin.shopify.com (transiently — typically while the iframe is between about:blank and the new document during reload). It's noise that pre-dates this incident and is not what's making the dashboard blank. Initial triage anchored on this error and the bundle's 31 % size drop and reached for the symptom rather than the route table. The synthetic-parent test that captured App Bridge messages was the key signal that flipped the diagnosis — the bundle WAS working, just rendering nothing.
Root cause
Vite 7 switched its default internal glob library from micromatch to tinyglobby for import.meta.glob. tinyglobby does not implement extglob negation (!(...)), so the pattern ./pages/**/!(*.test.[jt]sx)*.([jt]sx) silently matches zero files on Vite 7 and later. The Dependabot PR #3651 upgraded Vite from 5 to 8 in one step; the existing glob pattern in App.tsx was not updated to a tinyglobby-compatible form.
Fix
Extracted the pages glob into components/shopify/admin_ui/src/loadPages.ts
so the test below can exercise the same function App.tsx calls (instead of
duplicating the pattern):
// src/loadPages.ts
export function loadPages(): Record<string, PageModule> {
const allPages = import.meta.glob<PageModule>("./pages/**/*.{tsx,jsx}", {
eager: true,
});
return Object.fromEntries(
Object.entries(allPages).filter(([key]) => !/\.test\.[jt]sx$/.test(key)),
);
}
// src/App.tsx (now)
import { loadPages } from "./loadPages";
// ...
const pages = loadPages();
This works on both Vite 5 (micromatch) and Vite 8 (tinyglobby). Verified
locally that the rebuilt bundle restores the page modules (NotFound,
ExitIframe strings present in the entry bundle) and matches the previous
working bundle's size class (~563 KB vs broken 393 KB vs June 11 working
576 KB).
Regression guard
components/shopify/admin_ui/tests/loadPages.test.ts asserts that
loadPages():
- returns a non-empty record (catches silent zero-match globber regressions)
- includes the known core pages (
index.tsx,settings.tsx,NotFound.tsx,ExitIframe.tsx) - yields a function-valued
defaultexport for every entry - still excludes
*.test.[jt]sxfiles
The first two assertions fail loudly if the glob ever silently matches
nothing again. Manually verified: temporarily restoring the broken extglob
pattern in loadPages.ts makes 2/4 of those cases fail; restoring the fix
brings the full suite back to 50/50 green.
This is a unit-level guard; it does not require running a browser or rebuilding the bundle. The "Followups" below cover deeper layers.
Followups
- Add a build-output assertion to admin_ui CI. After
npm run build, run a headless browser that loadsdist/index.htmland asserts a known page route renders. ≤ 2 minutes of CI time, catches this class of silent regression at the post-bundle stage in addition to the unit-level guard. The currentnpm run buildstep verifies the bundler exited 0; that's not the same as the bundle being functional. - Add Dependabot major-version handling for admin_ui. Currently major bumps merge with the same CI as patches. Consider either grouping multi-major bumps for human review, or pinning major versions and ignoring them in
.github/dependabot.yml. - Add an e2e test that loads the embedded admin iframe. The existing
components/shopify/e2e_tests/Playwright suite covers storefront, install/uninstall, and admin worker — none of them loadadmin.ecom.marqo.ai/?embedded=1&shop=...and assert that a route renders. A single spec would block this exact regression class going forward. - Audit
import.meta.globusages across the repo for other extglob negation patterns that may break on Vite 7+. Same risk wherever extglob!(...)appears.