July 16, 2026
d5bb40f603e066508f4a0786fff417741cdd3ea56be54123be1c0bb5d80336ef Previous:
6d27951e Bundle: 91.9 KB Reworks save-bar dirty-state tracking, wires it into tools.register, and adds a version field to protocol messages.
Highlights
- The save-bar dirty-state utility in _utilities.js was rewritten with a new valuemodified event and a shared capture/settle registry for more reliable change detection.
- tools.register() now captures save-bar state before invoking a registered handler and settles it afterward via double requestAnimationFrame, keeping the save bar accurate when a tool action modifies form state.
- Bootstrap now attaches a version field to the client config, and web-vitals includes the same version field in its WebVitals protocol messages.
- resource-picker.js simplified how it builds the error thrown for an invalid type option, without changing the message text.
Infrastructure Changes
REPORT.md +10 -10
@@ -1,14 +1,14 @@
# Shopify App Bridge — Unminification Report
Generated: 2026-07-10T09:02:03.630Z
Generated: 2026-07-16T08:43:50.705Z
## Files
| File | Size | Lines | Type |
|------|------|-------|------|
| _bootstrap.js | 29.6KB | 1028 | Infrastructure |
| _bootstrap.js | 29.7KB | 1031 | Infrastructure |
| _remote-ui.js | 6.0KB | 255 | Infrastructure |
| _utilities.js | 73.7KB | 2754 | Infrastructure |
| _utilities.js | 74.4KB | 2798 | Infrastructure |
| _web-vitals.js | 11.6KB | 527 | Infrastructure |
| analytics.js | 211B | 11 | Module |
| app.js | 346B | 15 | Module |
@@ -18,14 +18,14 @@ Generated: 2026-07-10T09:02:03.630Z
| 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 | 416B | 19 | Module |
| navigation.js | 408B | 19 | Module |
| picker.js | 451B | 18 | Module |
| polaris.js | 240B | 12 | Module |
| pos.js | 5.7KB | 248 | Module |
| print.js | 560B | 25 | Module |
| print.js | 557B | 25 | Module |
| resource-picker.js | 2.8KB | 119 | Module |
| resource-picker.js | 2.8KB | 117 | Module |
| reviews.js | 365B | 16 | Module |
| s-app-nav.js | 175B | 10 | Module |
| s-app-window.js | 228B | 12 | Module |
@@ -40,13 +40,13 @@ Generated: 2026-07-10T09:02:03.630Z
| telemetry.js | 935B | 35 | Module |
| title-bar.js | 7.6KB | 290 | Module |
| toast.js | 1.6KB | 71 | Module |
| tools.js | 1.6KB | 57 | Module |
| tools.js | 2.2KB | 75 | Module |
| ui-modal.js | 153B | 10 | Module |
| ui-nav-menu.js | 164B | 10 | Module |
| user.js | 940B | 37 | Module |
| visibility.js | 973B | 34 | Module |
| web-vitals.js | 1.8KB | 64 | Module |
| web-vitals.js | 1.9KB | 66 | Module |
| **Total** | **173.3KB** | **6579** | |
| **Total** | **174.6KB** | **6644** | |
## Pipeline Stages
modules/_bootstrap.js Truncated +5 -5
@@ -168,7 +168,7 @@
removeEventListener: globalThis.removeEventListener.bind(globalThis),
postMessage: globalThis.parent.postMessage.bind(globalThis.parent),
});
const c = restoreProperty();
const c = ORIGINAL_SYMBOL();
const l = (function (t) {
const n = t.decodeSignal;
return (t) => {
@@ -215,7 +215,7 @@
type: t.type,
lastModified: t.lastModified,
},
content: gt(t),
content: checkPrivateField(t),
};
} else {
if (Array.isArray(t)) {
@@ -251,7 +251,7 @@
typeof t.content == 'string'
);
})(t)
? new File([bt(t.content)], t.metadata.name, {
? new File([privateKeyCounter(t.content)], t.metadata.name, {
type: t.metadata.type,
lastModified: t.metadata.lastModified,
})
@@ -399,6 +399,7 @@
config: t,
protocol: h,
origin: o,
version: Lt,
data: {},
setSignals(t) {
c.applyRealSignal(t);
@@ -433,8 +434,8 @@
{
I(
{
fetch: Lt,
fetch: safeAsyncCall,
},
[],
);
@@ -651,7 +652,9 @@
}
});
}
h.send('Client.initialize');
... (truncated)
Diff truncated at 200 lines
modules/_utilities.js Truncated +58 -22
@@ -205,45 +205,58 @@ const W = 'data-save-bar';
const V = 'data-discard-confirmation';
const z = 'ui-save-bar';
const H = 'update';
function G(t, { onChange, filter: e = () => true }) {
const G = 'valuemodified';
function i() {
const set = new Set();
const i = tt(t).filter((t) => X(t) && e(t));
function K() {
for (const t of set) t.settle();
}
function X(t, { onChange, filter: e = () => true }) {
let i = false;
let o = null;
function r() {
return it(t).filter((t) => Z(t) && e(t));
}
function a(t) {
return (t[N] === true || i) && tt(t);
}
function s() {
const e = r();
let i = false;
let o = false;
let r = false;
for (const t of e) {
for (const t of i) {
i = [].some.call(t.elements, a);
o = [].some.call(t.elements, Y);
if (i) break;
if (o) break;
}
}
? {
saveButton: {
onAction: () =>
(function (t) {
}
})(t),
},
discardButton: {
onAction: () =>
(function (t) {
}
})(t),
},
}
: undefined;
}
const n = 'target' in t ? t.target : t;
if (n) {
if (!n[N]) {
@@ -262,26 +275,30 @@ function G(t, { onChange, filter: e = () => true }) {
}
}
}
const n = t.target;
if (n) {
if (n)
n[N] = true;
if (t.type !== G) {
i();
n[N] = true;
}
s();
} else {
if (!i) return;
s();
}
}
const n = t.target;
for (const t of n.elements) {
t[N] = false;
}
}
}
const n = t.target;
for (const t of n.elements) {
if (D in t && 'values' in t) {
t.values = t[D];
@@ -298,24 +315,49 @@ function G(t, { onChange, filter: e = () => true }) {
t.checked = t[q];
}
t[N] = false;
t[N] = false;
}
}
}
i();
const f = {
capture: function () {
if (!i) {
o = new Set();
for (const t of r())
for (const n of t.elements) {
c(n);
o.add(n);
}
i = true;
}
},
settle: function () {
if (o)
for (const t of o)
if (!t[N] && tt(t)) {
t[N] = true;
}
i = false;
o = null;
s();
},
};
set.add(f);
s();
const mutationObserver = new MutationObserver((t) => {
for (const n of t)
});
t.addEventListener('input', r);
t.addEventListener(G, u);
t.addEventListener('change', r);
t.addEventListener('input', u);
t.addEventListener('beforeinput', o);
t.addEventListener('change', u);
t.addEventListener('focusin', o);
t.addEventListener('beforeinput', c);
t.addEventListener('focusin', c);
mutationObserver.observe(t, {
subtree: true,
childList: true,
@@ -323,19 +365,21 @@ function G(t, { onChange, filter: e = () => true }) {
attributeFilter: [W, V],
});
return {
unobserve: () => {
mutationObserver.disconnect();
t.removeEventListener('focusin', o);
t.removeEventListener('beforeinput', o);
t.removeEventListener('change', r);
t.removeEventListener('input', r);
t.removeEventListener('submit', a);
t.removeEventListener('reset', s);
... (truncated)
Diff truncated at 200 lines
Module Changes
modules/fetch.js +3 -3
@@ -3,8 +3,8 @@
* Intercepted fetch with auth headers and session token refresh
*/
// Registry entry referenced as: Lt
// Registry entry referenced as: safeAsyncCall
const Lt = ({ api, protocol, internalApiPromise }) => {
const safeAsyncCall = ({ api, protocol, internalApiPromise }) => {
const i = self.fetch;
async function o() {
try {
@@ -90,4 +90,4 @@ const Lt = ({ api, protocol, internalApiPromise }) => {
});
};
const fetchModule = Lt;
const fetchModule = safeAsyncCall;
modules/id-token.js +1 -1
@@ -3,8 +3,8 @@
* Session token (ID token) request/response
*/
// Registry entry referenced as: It
// Registry entry referenced as: xt
api.idToken = async function () {
const { idToken: t } = (await internalApiPromise) || {};
const i = t
modules/intents.js +2 -2
@@ -44,7 +44,7 @@ const intentsModule = ({ api, protocol, internalApiPromise, signalFactory }) =>
'AppFrame.propertiesEvent',
({ properties }) => {
const o = (function (t, n, e) {
return new deepClone('configure', 'gid://flow/stepReference/' + t, n, () =>
return new WINDOW_TARGETS('configure', 'gid://flow/stepReference/' + t, n, () =>
e.send('AppFrame.navigateBack'),
);
})(
@@ -67,7 +67,7 @@ const intentsModule = ({ api, protocol, internalApiPromise, signalFactory }) =>
if (!i) throw Error('Cannot invoke intent');
if (!i.intents?.invoke || typeof i.intents.invoke != 'function')
throw Error('Intents are not supported');
return new safeAsyncCall(i.intents.invoke(t, n));
return new Ut(i.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 i = xt(e);
const i = ALL_PROTOCOLS(e);
const o = await internalApiPromise;
if (o && o.internalModal) {
await o.internalModal.show?.(t, i);
modules/navigation.js +1 -1
@@ -4,13 +4,13 @@
*/
const navigationModule = (t) => {
t.internalApiPromise.then((e) => {
const { navigation: i } = e || {};
if (i?.version === 2)
try {
n();
generateId(t);
Yt(t);
} catch (err) {
console.error('Failed to set up navigation: ' + err);
}
modules/print.js +1 -1
@@ -5,9 +5,9 @@
const printModule = ({ protocol, internalApiPromise }) => {
if (k() || P()) {
const e = document.scrollingElement?.scrollHeight || document.body.offsetHeight;
SHOPIFY_PROTOCOLS(async () => {
CLICKABLE_TAGS(async () => {
const { print: i } = (await internalApiPromise) || {};
if (i) {
await i({
modules/resource-picker.js +2 -4
@@ -9,20 +9,18 @@ const resourcePickerModule = ({ api, protocol, internalApiPromise }) => {
if (i) return i.finally(() => t(o));
const r = await internalApiPromise;
const promise = new Promise((t, e) => {
const {
type: s,
query: c,
selectionIds: u,
action: l,
multiple: d,
if (!set.has(s))
if (!on.has(s))
return e(
Error(
Error("The 'type' option for resourcePicker must be one of " + Array.from(on).join(', ')),
"The 'type' option for resourcePicker must be one of " + Array.from(set).join(', '),
),
);
const abortController = new AbortController();
const { signal: h } = abortController;
modules/s-app-nav.js +1 -1
@@ -3,7 +3,7 @@
* Custom <s-app-nav> element for app navigation
*/
// Registry entry referenced as: hn
// Registry entry referenced as: bn
modules/s-app-window.js +1 -1
@@ -3,9 +3,9 @@
* Custom <s-app-window> element for app window management
*/
// Registry entry referenced as: Xn
// Registry entry referenced as: te
variantLock: 'app-window',
});
modules/sidekick.js +2 -2
@@ -115,7 +115,7 @@ const sidekickModule = ({ api, internalApiPromise, rpcEventTarget }) => {
s(e);
e = undefined;
}
SHOPIFY_PROTOCOLS(async () => {
CLICKABLE_TAGS(async () => {
const { sidekick: a } = (await internalApiPromise) || {};
if (!a || typeof a.registerToolHandler != 'function')
throw Error('Sidekick API is not available');
@@ -136,7 +136,7 @@ const sidekickModule = ({ api, internalApiPromise, rpcEventTarget }) => {
s(e);
e = undefined;
}
SHOPIFY_PROTOCOLS(async () => {
CLICKABLE_TAGS(async () => {
const { sidekick: a } = (await internalApiPromise) || {};
if (!a || typeof a.registerContextCallback != 'function')
throw Error('Sidekick API is not available');
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 = `${Un}, ${_n}, ${Bn}`;
const e = `${jn}, ${Dn}, ${qn}`;
if (i) return void i.click();
const o = Array.from(document.querySelectorAll('s-menu, s-button-group'));
for (const n of o) {
if (e) return void e.click();
}
}
i?.click();
}
function a(t) {
modules/toast.js +2 -2
@@ -6,12 +6,12 @@
const toastModule = ({ api, protocol, internalApiPromise }) => {
api.toast = {
show(t, i = {}) {
SHOPIFY_PROTOCOLS(async () => {
CLICKABLE_TAGS(async () => {
const { toast: r } = (await internalApiPromise) || {};
if (r?.show)
await r.show(t, {
...i,
id: o,
});
@@ -55,7 +55,7 @@ const toastModule = ({ api, protocol, internalApiPromise }) => {
return o;
},
hide(t) {
SHOPIFY_PROTOCOLS(async () => {
CLICKABLE_TAGS(async () => {
const { toast: i } = (await internalApiPromise) || {};
if (i?.hide) {
await i.hide(t);
modules/tools.js +22 -4
@@ -8,12 +8,30 @@ const toolsModule = async ({ api, internalApiPromise }) => {
api.tools = {
register(t, o) {
const abortController = new AbortController();
SHOPIFY_PROTOCOLS(async () => {
CLICKABLE_TAGS(async () => {
const { tools: i } = (await internalApiPromise) || {};
if (abortController.signal.aborted) return;
if (!i || typeof i.register != 'function') throw Error('Tools API is not available');
map.get(t)?.();
const a = await i.register(t, o);
const a = await i.register(
t,
(function (t) {
return async (...n) => {
(function () {
for (const t of set) t.capture();
})();
try {
return await t(...n);
} finally {
if (typeof requestAnimationFrame == 'function') {
requestAnimationFrame(() => requestAnimationFrame(() => K()));
} else {
setTimeout(() => K(), 0);
}
}
};
})(o),
);
if (!abortController.signal.aborted) {
map.set(t, a);
}
@@ -31,7 +49,7 @@ const toolsModule = async ({ api, internalApiPromise }) => {
i.add(abortController);
map.get(t)?.();
map.delete(t);
SHOPIFY_PROTOCOLS(async () => {
CLICKABLE_TAGS(async () => {
const { tools: e } = (await internalApiPromise) || {};
if (!abortController.signal.aborted) {
if (!e || typeof e.unregister != 'function') throw Error('Tools API is not available');
@@ -46,7 +64,7 @@ const toolsModule = async ({ api, internalApiPromise }) => {
map.clear();
i.forEach((t) => t.abort());
i.clear();
SHOPIFY_PROTOCOLS(async () => {
CLICKABLE_TAGS(async () => {
const { tools: t } = (await internalApiPromise) || {};
if (!t || typeof t.clear != 'function') throw Error('Tools API is not available');
await t.clear();
modules/ui-modal.js +1 -1
@@ -3,7 +3,7 @@
* Custom <ui-modal> element
*/
// Registry entry referenced as: re
// Registry entry referenced as: ue
modules/ui-nav-menu.js +1 -1
@@ -3,7 +3,7 @@
* Custom <ui-nav-menu> element
*/
// Registry entry referenced as: ae
// Registry entry referenced as: le
modules/web-vitals.js +2 -0
@@ -33,12 +33,14 @@ const webVitalsModule = async ({ api, protocol, internalApiPromise, rpcEventTarg
id: i.id,
name: i.name,
value: i.value,
version: Lt,
});
} else {
protocol.send('WebVitals.' + t, {
id: i.id,
metricName: i.name,
value: i.value,
version: Lt,
});
}
if (l && i.name === 'LCP') {