All entries

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.
19 files changed +117 -63

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') {