August 5, 2026
f90ae58509b27d428e1462e51ea8606bd4196b3af2b97599d8067dea1995f5fe Previous:
740e2544 Bundle: 93.1 KB App nav (v0.38.7) now validates anchor hrefs and warns on unsupported links instead of silently breaking.
Highlights
- App nav parsing (used by s-app-nav and ui-nav-menu) now validates each anchor href and only accepts relative app paths, app: URLs, or same-origin http(s) URLs.
- Anchors with unsupported hrefs are dropped from the nav and logged once via console.warn with a link to the app-nav docs, instead of silently producing a broken URL.
- app: protocol URLs are now converted to a proper same-origin URL string before being sent in the Menu update payload.
- The internal-only modal show() method and the intents module's configure/invoke helpers were renamed internally with no behavioral change.
- Bumped the internal app-bridge utilities version from 0.38.6 to 0.38.7.
Infrastructure Changes
REPORT.md +4 -4
@@ -1,6 +1,6 @@
# Shopify App Bridge — Unminification Report
Generated: 2026-07-30T08:52:07.704Z
Generated: 2026-08-05T08:53:06.590Z
## Files
@@ -8,7 +8,7 @@ Generated: 2026-07-30T08:52:07.704Z
|------|------|-------|------|
| _bootstrap.js | 29.7KB | 1033 | Infrastructure |
| _remote-ui.js | 6.0KB | 255 | Infrastructure |
| _utilities.js | 75.1KB | 2815 | Infrastructure |
| _utilities.js | 76.0KB | 2848 | Infrastructure |
| _web-vitals.js | 11.6KB | 527 | Infrastructure |
| analytics.js | 211B | 11 | Module |
| app.js | 346B | 15 | Module |
@@ -18,7 +18,7 @@ Generated: 2026-07-30T08:52:07.704Z
| id-token.js | 908B | 34 | Module |
| index.js | 2.2KB | 48 | Index |
| intents.js | 3.1KB | 106 | Module |
| internal-only.js | 528B | 26 | Module |
| internal-only.js | 539B | 26 | Module |
| loading.js | 605B | 31 | Module |
| navigation.js | 408B | 19 | Module |
| picker.js | 451B | 18 | Module |
@@ -46,7 +46,7 @@ Generated: 2026-07-30T08:52:07.704Z
| user.js | 940B | 37 | Module |
| visibility.js | 973B | 34 | Module |
| web-vitals.js | 1.9KB | 66 | Module |
| **Total** | **175.3KB** | **6663** | |
| **Total** | **176.2KB** | **6696** | |
## Pipeline Stages
modules/_bootstrap.js +5 -5
@@ -411,7 +411,7 @@
value: y,
});
const b = new rt();
const v = subscribeAllErrors();
const v = U();
const P = v.promise.then((t) => t?.internal);
const C = k() && !S() && window === top;
if ((top === window && !E() && !y.config.disabledFeatures?.includes('auto-redirect')) || C)
@@ -971,43 +971,43 @@
h.send('Loading.stop');
y.ready = Promise.resolve();
})();
var he;
var me;
var we;
var ye;
var be;
var ve = -1;
var ve;
var ge = function (t) {
var ge;
var Ae = -1;
var Ee = function (t) {
addEventListener(
'pageshow',
function (n) {
if (n.persisted) {
t(n);
}
},
true,
);
};
return (
window.performance &&
performance.getEntriesByType &&
performance.getEntriesByType('navigation')[0]
);
};
return (t && t.activationStart) || 0;
};
var o = 'navigate';
o = 'back-forward-cache';
} else {
if (e) {
o = 'prerender';
} else {
if (document.wasDiscarded) {
modules/_utilities.js Truncated +49 -22
@@ -106,7 +106,7 @@ function $(t, n, e) {
t.subscribe('Error.' + o, n, e);
});
}
function subscribeAllErrors() {
function U() {
let t;
let n = false;
const promise = new Promise((n) => {
@@ -125,7 +125,7 @@ function subscribeAllErrors() {
},
};
}
function _() {
function subscribeAllErrors() {
let t = Promise.resolve();
const n = {};
return {
@@ -134,7 +134,7 @@ function _() {
},
has: (t) => !!n[t],
add(e) {
const o = subscribeAllErrors();
const o = U();
n[e] = o;
t = t.then(() => o.promise);
},
@@ -726,7 +726,7 @@ function ORIGINAL_SYMBOL(t = false) {
};
}
ORIGINAL_SYMBOL(true);
const Lt = '0.38.6+00fb5d58';
const Lt = '0.38.7+8dcfe0ce';
const It = Symbol();
function Mt(t, n, e) {
const o = t[n];
@@ -861,7 +861,7 @@ const xt = ({ api, protocol, internalApiPromise }) => {
};
};
const SHOPIFY_PROTOCOLS = Symbol();
class WINDOW_TARGETS {
class Ut {
constructor(t, n, e, o) {
this.action = t;
this.type = n;
@@ -872,19 +872,19 @@ class WINDOW_TARGETS {
this[SHOPIFY_PROTOCOLS]();
}
}
class ALL_PROTOCOLS {
class WINDOW_TARGETS {
constructor(t) {
this.complete = t;
}
}
function Ut(t) {
function ALL_PROTOCOLS(t) {
return typeof t != 'object' || t === null
? t
: Array.isArray(t)
? t.map((t) => Ut(t))
? t.map((t) => ALL_PROTOCOLS(t))
: Object.keys(t).reduce((n, e) => {
const o = t[e];
n[e] = Ut(o);
n[e] = ALL_PROTOCOLS(o);
return n;
}, {});
}
@@ -1496,6 +1496,19 @@ const yn = {
target: hn,
};
function bn(t) {
const n = t.getAttribute('href');
if (n)
try {
const t = Vt(n);
if (t.protocol === 'app:' || t.origin === location.origin) return t;
} catch {}
}
function vn(t) {
return t.protocol === 'app:'
? new URL(`${t.pathname}${t.search}`, location.origin).toString()
: t.toString();
}
function gn(t) {
const n = {
name: t,
attributes: {},
@@ -1513,37 +1526,51 @@ function bn(t) {
],
};
return ({ protocol, internalApiPromise }) => {
async function i() {
const i = new Set();
function r(n) {
const e = n.getAttribute('href') ?? ('href' in n ? n.href : '');
if (!i.has(e)) {
i.add(e);
console.warn(
`[${t}] Dropped nav item with unsupported href "${e}". App nav only supports relative app paths (e.g. "/settings"), app: URLs, or same-origin http(s) URLs. See https://shopify.dev/docs/api/app-home/app-bridge-web-components/app-nav`,
);
}
}
async function a() {
const { navigation: t } = (await internalApiPromise) || {};
return !(t?.version !== 2);
}
}
}
const n = Array.from(document.querySelectorAll(t)).reverse()[0];
const t = n.anchors
const t = [];
.filter((t) => !n.isHomeAnchor(t))
for (const e of n.anchors) {
.map((t) => {
if (n.isHomeAnchor(e)) continue;
const { pathname: n, search: e, textContent: o, rel: i } = t;
const o = bn(e);
return {
if (o) {
label: o ?? '',
t.push({
url: new URL(n + e, location.href).toString(),
label: e.textContent ?? '',
rel: i || undefined,
url: vn(o),
};
rel: e.rel || undefined,
a.set(t);
} else {
if (await i()) return;
r(e);
}
}
i.set(t);
if (await a()) return;
}
const u = document.querySelectorAll('s-app-nav, ui-nav-menu');
if (u.length > 1) {
console.warn(
@@ -1554,10 +1581,10 @@ function bn(t) {
.join(', ')}`,
);
}
items: Array.from(document.querySelectorAll(t)).flatMap((t) => t.menuItems()),
};
protocol.send(`Menu.${s}_Menu.UPDATE`, l);
protocol.send(`Menu.${c}_Menu.UPDATE`, f);
}
const promise = new Promise((t) => {
protocol.subscribe(
Diff truncated at 200 lines
Module Changes
modules/intents.js +2 -2
@@ -44,7 +44,7 @@ const intentsModule = ({ api, protocol, internalApiPromise, signalFactory }) =>
'AppFrame.propertiesEvent',
({ properties }) => {
const i = (function (t, n, e) {
return new WINDOW_TARGETS('configure', 'gid://flow/stepReference/' + t, n, () =>
return new Ut('configure', 'gid://flow/stepReference/' + t, n, () =>
e.send('AppFrame.navigateBack'),
);
})(
@@ -67,7 +67,7 @@ const intentsModule = ({ api, protocol, internalApiPromise, signalFactory }) =>
if (!o) throw Error('Cannot invoke intent');
if (!o.intents?.invoke || typeof o.intents.invoke != 'function')
throw Error('Intents are not supported');
return new ALL_PROTOCOLS(o.intents.invoke(t, n));
return new WINDOW_TARGETS(o.intents.invoke(t, n));
},
};
internalApiPromise.then((t) => {
modules/internal-only.js +1 -1
@@ -6,7 +6,7 @@
const internalOnlyModule = ({ api, internalApiPromise }) => {
const e = {
async show(t, e) {
const o = Ut(e);
const o = ALL_PROTOCOLS(e);
const i = await internalApiPromise;
if (i && i.internalModal) {
await i.internalModal.show?.(t, o);
modules/pos.js +4 -4
@@ -8,7 +8,7 @@ const posModule = ({ api, protocol }) => {
async function o(t, o) {
const i = on();
const abortController = new AbortController();
const a = subscribeAllErrors();
const a = U();
protocol.subscribe(
'Cart.update',
({ data }) => {
@@ -32,7 +32,7 @@ const posModule = ({ api, protocol }) => {
async fetch() {
const t = on();
const abortController = new AbortController();
const o = subscribeAllErrors();
const o = U();
protocol.send('Cart.fetch', {
id: t,
});
@@ -197,7 +197,7 @@ const posModule = ({ api, protocol }) => {
protocol.send('Pos.close');
},
async device() {
const t = subscribeAllErrors();
const t = U();
protocol.subscribe(
'getState',
({ pos }) => {
@@ -215,7 +215,7 @@ const posModule = ({ api, protocol }) => {
return t.promise;
},
async location() {
const t = subscribeAllErrors();
const t = U();
protocol.subscribe(
'getState',
({ pos }) => {
modules/s-app-nav.js +1 -1
@@ -3,7 +3,7 @@
* Custom <s-app-nav> element for app navigation
*/
// Registry entry referenced as: vn
// Registry entry referenced as: An
modules/s-app-window.js +1 -1
@@ -3,9 +3,9 @@
* Custom <s-app-window> element for app window management
*/
// Registry entry referenced as: ne
// Registry entry referenced as: oe
variantLock: 'app-window',
});
modules/title-bar.js +1 -1
@@ -24,16 +24,16 @@ const titleBarModule = ({ protocol, internalApiPromise }) => {
function r(t) {
const n = document.querySelector('s-page');
if (n) {
const e = `${Dn}, ${qn}, ${Wn}`;
const e = `${Wn}, ${Hn}, ${Vn}`;
if (o) return void o.click();
const i = Array.from(document.querySelectorAll('s-menu, s-button-group'));
for (const n of i) {
if (e) return void e.click();
}
}
o?.click();
}
function a(t) {
modules/ui-modal.js +1 -1
@@ -3,7 +3,7 @@
* Custom <ui-modal> element
*/
// Registry entry referenced as: le
// Registry entry referenced as: de
modules/ui-nav-menu.js +1 -1
@@ -3,7 +3,7 @@
* Custom <ui-nav-menu> element
*/
// Registry entry referenced as: fe
// Registry entry referenced as: pe