October 8, 2026
fdd8d9944e74ed2cb992b6b08fcae765246b7f4b886cda20feeb67a240117b82 Previous:
c23dfa58 Bundle: 97.7 KB Adds app-modal module that detects open Polaris modals, allowlists Shopify CDN hosts, and extends telemetry.
Highlights
- New app-modal module watches the Polaris portals container and notifies the host through appModal.setOpen when a Polaris modal opens or closes.
- Script tag validation now accepts any host from a Shopify CDN allowlist (cdn.shopify.com plus shopify-assets domains) over HTTPS instead of only cdn.shopify.com.
- The fetch module uses the same CDN allowlist check when deciding whether a request targets the Shopify CDN.
- Telemetry now reports reactMajorVersion for ui-component usage and exposes a global hook so already-connected custom elements are also counted.
Infrastructure Changes
REPORT.md +13 -12
@@ -1,24 +1,25 @@
# Shopify App Bridge — Unminification Report
Generated: 2026-10-01T08:04:35.805Z
Generated: 2026-10-08T08:06:15.850Z
## Files
| File | Size | Lines | Type |
|------|------|-------|------|
| _bootstrap.js | 30.9KB | 1076 | Infrastructure |
| _bootstrap.js | 30.7KB | 1071 | Infrastructure |
| _remote-ui.js | 6.1KB | 257 | Infrastructure |
| _remote-ui.js | 6.4KB | 267 | Infrastructure |
| _utilities.js | 78.0KB | 2924 | Infrastructure |
| _utilities.js | 78.2KB | 2930 | Infrastructure |
| _web-vitals.js | 11.6KB | 527 | Infrastructure |
| analytics.js | 211B | 11 | Module |
| app-modal.js | 1.2KB | 60 | Module |
| app.js | 346B | 15 | Module |
| client.js | 303B | 15 | Module |
| environment.js | 241B | 14 | Module |
| fetch.js | 3.0KB | 94 | Module |
| id-token.js | 929B | 34 | Module |
| id-token.js | 941B | 34 | Module |
| index.js | 2.2KB | 48 | Index |
| index.js | 2.2KB | 49 | Index |
| intents.js | 3.1KB | 106 | Module |
| internal-only.js | 534B | 26 | Module |
| internal-only.js | 528B | 26 | Module |
| loading.js | 605B | 31 | Module |
| navigation.js | 408B | 19 | Module |
| picker.js | 451B | 18 | Module |
@@ -29,15 +30,15 @@ Generated: 2026-10-01T08:04:35.805Z
| reviews.js | 365B | 16 | Module |
| s-app-nav.js | 175B | 10 | Module |
| s-app-window.js | 228B | 12 | Module |
| save-bar.js | 3.4KB | 138 | Module |
| save-bar.js | 3.5KB | 138 | Module |
| scanner.js | 2.9KB | 101 | Module |
| scanner.js | 2.8KB | 101 | Module |
| scopes.js | 797B | 26 | Module |
| share.js | 1.3KB | 58 | Module |
| shopifyQL.js | 1.2KB | 55 | Module |
| shortcut.js | 474B | 24 | Module |
| shortcut.js | 461B | 24 | Module |
| sidekick.js | 5.3KB | 179 | Module |
| support.js | 508B | 21 | Module |
| telemetry.js | 935B | 35 | Module |
| telemetry.js | 1.1KB | 46 | Module |
| title-bar.js | 8.4KB | 317 | Module |
| toast.js | 1.6KB | 71 | Module |
| tools.js | 2.2KB | 75 | Module |
@@ -46,7 +47,7 @@ Generated: 2026-10-01T08:04:35.805Z
| user.js | 940B | 37 | Module |
| visibility.js | 973B | 34 | Module |
| web-vitals.js | 1.9KB | 66 | Module |
| **Total** | **181.9KB** | **6912** | |
| **Total** | **183.5KB** | **6995** | |
## Pipeline Stages
modules/_bootstrap.js Truncated +19 -26
@@ -19,30 +19,25 @@
? (console.error('The script tag loading App Bridge has `async`'), false)
: t.defer
? (console.error('The script tag loading App Bridge has `defer`.'), false)
: t.src
: t.src && n(new URL(t.src))
? new URL(t.src).hostname != mt
? ([...document.scripts]
? (console.error(
.filter((t) =>
'The script tag loading App Bridge is not loading App Bridge from the Shopify CDN.',
(function (t) {
return (
t.src &&
!t.defer &&
!t.async &&
t.type !== 'module' &&
!t.dataset.appBridgeCompatible &&
/^ *(|(text|application)\/(x-)?(java|ecma)script) *$/i.test(t.type)
);
})(t),
)
.indexOf(t) !== 0 &&
console.warn(
'The script tag loading App Bridge should be the first script tag in the document. Loading other blocking scripts first can cause unexpected behavior.',
),
false)
true)
: ([...document.scripts]
.filter((t) =>
(function (t) {
return (
t.src &&
!t.defer &&
!t.async &&
t.type !== 'module' &&
!t.dataset.appBridgeCompatible &&
/^ *(|(text|application)\/(x-)?(java|ecma)script) *$/i.test(t.type)
);
})(t),
)
.indexOf(t) !== 0 &&
console.warn(
'The script tag loading App Bridge should be the first script tag in the document. Loading other blocking scripts first can cause unexpected behavior.',
),
true)
: (console.error(
'The script tag loading App Bridge is not loading App Bridge from the Shopify CDN.',
),
@@ -75,52 +70,52 @@
}
})(),
);
(function () {
const t = Array.from(document.getElementsByTagName('script'));
if (document.currentScript) {
t.unshift(document.currentScript);
}
try {
if (url.hostname === mt && lt.test(url.pathname)) {
if (n(url) && dt.test(url.pathname)) {
if (t) {
}
});
}
} catch (err) {}
try {
} catch (err) {
console.warn('App Bridge Next: failed to parse configuration. ' + err);
}
})(),
);
(function () {
const t = Array.from(document.querySelectorAll('meta[name^="shopify-"i]'));
const n = {};
for (const e of t) {
if (!e.hasAttribute('name')) continue;
}
return n;
})(),
);
(function (t) {
return {
shop: t.get('shop'),
@@ -129,47 +124,47 @@
};
})(searchParams),
);
if (n.length !== 0)
throw Error('App Bridge Next: missing required configuration fields: ' + n);
return t;
return {
params: searchParams,
};
})();
(function (t) {
try {
sessionStorage.setItem('app-bridge-config', JSON.stringify(t));
} catch (err) {}
try {
} catch {}
}
try {
} catch {
return;
}
})();
}
const i = e.host ? atob(e.host) : e.shop;
const a = new URL('https://' + i).origin;
... (truncated)
Diff truncated at 200 lines
modules/_remote-ui.js Truncated +13 -3
@@ -4,15 +4,25 @@
* @description Remote procedure call and memory management for remote rendering
*/
const t = 'admin_theme';
const set = new Set([
const n = 'app-bridge-admin-theme';
'cdn.shopify.com',
'shopify-assets.shopifycdn.com',
'shopify-assets.shopifystagingcdn.cn',
'shopify-assets.shopifycdn.cn',
'shopify-assets.alejandrochinashopifytest742501.com',
]);
function n(n) {
return n.protocol === 'https:' && set.has(n.hostname.toLowerCase());
}
const e = 'admin_theme';
const o = 'app-bridge-admin-theme';
const retainSymbol =
Symbol.for('RemoteUi::Retain'); /* Symbol for retaining a remote object in memory */
const releaseSymbol =
Symbol.for('RemoteUi::Release'); /* Symbol for releasing a remote object from memory */
const retainedBySymbol =
Symbol.for('RemoteUi::RetainedBy'); /* Symbol for tracking which retainers hold this object */
constructor() {
this.memoryManaged = new Set();
}
@@ -29,186 +39,186 @@ class r {
this.memoryManaged.clear();
}
}
return !!(t && t[retainSymbol] && t[releaseSymbol]);
}
}
if (r) {
t[retainSymbol]();
}
if (n) {
if (Array.isArray(t)) {
}
}
}
return r;
}
}
t[releaseSymbol]();
}
if (n) {
if (Array.isArray(t)) {
e.set(t, o);
return o;
}
const o = Object.keys(t).reduce((o, i) => l(t[i], n, e) || o, r);
const o = Object.keys(t).reduce((o, i) => d(t[i], n, e) || o, i);
e.set(t, o);
return o;
}
}
}
if (t == null || typeof t != 'object') return false;
const n = Object.getPrototypeOf(t);
return n == null || n === Object.prototype;
}
const remoteFunctionMarker = '_@f'; /* Function marker for RPC serialization */
const map = new Map();
return {
const t = [];
const n = [
}),
t,
];
return n;
}
const t = [];
const n = [
t.push(...s);
return {
...n,
};
}, {}),
t,
];
return n;
}
}
const e = [
{
[remoteFunctionMarker]: t,
},
];
... (truncated)
Diff truncated at 200 lines
modules/_utilities.js Truncated +26 -26
@@ -4,16 +4,16 @@
* error types, DOM validation framework, navigation menu base
*/
return `${w()}-${w()}-${w()}-${w()}`;
return `${b()}-${b()}-${b()}-${b()}`;
}
return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(16);
}
return t.toLowerCase().replace(/-+(.)/g, (t, n) => n.toUpperCase());
}
if (n)
for (let e in n) {
const o = n[e];
@@ -36,41 +36,41 @@ class A extends g {
}).innerHTML = '<style>:host{display: none;}</style><slot></slot>';
}
}
return t.replace(/-([a-z])/gi, (t, n) => n.toUpperCase());
}
try {
return navigator.userAgent.toLowerCase().includes(t.toLowerCase());
} catch (err) {
return false;
}
}
}
function S() {
return k('Shopify Mobile');
}
function T() {
return k('Extensibility');
return S('Shopify Mobile');
}
return k('Shopify POS');
return S('Extensibility');
}
const L = 'app-iframe';
function I() {
const I = /frame:\/*([^/]+)\/([^/]+)(?:\/([^/]+))?(?:\/([^/]+))?$/;
return S('Shopify POS');
const $ = (() => {
}
const [, t, n, e] = window.name.match(I) ?? [];
const $ = 'app-iframe';
const M = /frame:\/*([^/]+)\/([^/]+)(?:\/([^/]+))?(?:\/([^/]+))?$/;
const R = (() => {
const [, t, n, e] = window.name.match(M) ?? [];
return {
apiKey: t,
scope: n,
mode: e,
};
})();
'hmac',
'locale',
'protocol',
@@ -82,14 +82,14 @@ const x = [
'embedded',
'appLoadId',
'link_source',
];
function O(t) {
function subscribeAllErrors(t) {
const url = new URL(t);
return url;
}
'FailedAuthentication',
'InvalidAction',
'InvalidActionType',
@@ -102,12 +102,12 @@ const _ = [
'UnexpectedAction',
'UnsupportedOperation',
];
function subscribeAllErrors(t, n, e) {
function j(t, n, e) {
t.subscribe('Error.' + o, n, e);
});
}
let t;
let n = false;
const promise = new Promise((n) => {
@@ -126,7 +126,7 @@ function U() {
},
};
}
function j() {
function createDeferred() {
let t = Promise.resolve();
const n = {};
return {
@@ -152,7 +152,7 @@ function j() {
},
};
}
function createDeferred({ keys, held, handler, keyEvent: o = 'keydown' }) {
function N({ keys, held, handler, keyEvent: o = 'keydown' }) {
let i = [];
const r = (o) => {
if (keys.flat().includes(o.key)) {
@@ -194,31 +194,31 @@ function createDeferred({ keys, held, handler, keyEvent: o = 'keydown' }) {
});
};
}
const t = window.shopify.config.host;
return 'https://' + atob(t);
}
const N = Symbol();
const q = Symbol();
const V = Symbol();
const W = Symbol();
const H = 'data-save-bar';
const H = Symbol();
const z = 'data-discard-confirmation';
const z = Symbol();
const J = 'ui-save-bar';
const J = 'data-save-bar';
const X = 'update';
const X = 'data-discard-confirmation';
const Q = 'valuemodified';
const Q = 'ui-save-bar';
const set = new Set();
const Y = 'update';
function G() {
const G = 'valuemodified';
for (const t of set) t.settle();
const K = new Set();
}
function Z() {
function K(t, { onChange, filter: e = () => true }) {
for (const t of K) t.settle();
}
function tt(t, { onChange, filter: e = () => true }) {
let o = false;
... (truncated)
Diff truncated at 200 lines
modules/index.js +1 -0
@@ -12,6 +12,7 @@
// Modules
// analytics.js
// app.js — App lifecycle and state management
// app-modal.js
// client.js — Client initialization and RPC connection
// environment.js — Environment detection (embedded, mobile, POS)
// fetch.js — Intercepted fetch with auth headers and session token refresh
Module Changes
modules/fetch.js +4 -4
@@ -3,17 +3,17 @@
* Intercepted fetch with auth headers and session token refresh
*/
// Registry entry referenced as: xt
// Registry entry referenced as: deepClone
const xt = ({ api, protocol, internalApiPromise }) => {
const deepClone = ({ api, protocol, internalApiPromise }) => {
try {
return await api.idToken();
} catch (err) {
return void console.error('Failed to fetch an idToken', err);
}
}
const e = new Headers(n.headers).get('Shopify-Challenge-Required');
return e && t?.isChallengeUrl && (await t.isChallengeUrl(e)) && t?.startChallenge
? {
@@ -23,19 +23,19 @@ const xt = ({ api, protocol, internalApiPromise }) => {
verified: false,
};
}
const url = new URL(request.url);
(url.protocol === location.protocol &&
(url.hostname === location.hostname || url.hostname.endsWith('.' + location.hostname))) ||
const d = url.hostname === 'cdn.shopify.com';
const p = n(url);
const t = Array.from(request.headers.entries());
if (n?.intercept) {
const n = {
method: request.method,
@@ -43,51 +43,51 @@ const xt = ({ api, protocol, internalApiPromise }) => {
headers: t,
body: (await request.text()) ?? undefined,
};
if (o) {
return new Response(t.body, t);
}
return new Response(e.body, e);
}
}
if (t) {
request.headers.set('Authorization', 'Bearer ' + t);
}
}
request.headers.set('X-Requested-With', 'XMLHttpRequest');
}
request.headers.set('Accept-Language', api.config.locale);
}
if (t) {
}
}
protocol.send('Navigation.redirect.remote', {
});
return new Promise(() => {});
}
}
});
};
const fetchModule = xt;
const fetchModule = deepClone;
modules/id-token.js +3 -3
@@ -3,8 +3,8 @@
* Session token (ID token) request/response
*/
// Registry entry referenced as: deepClone
// Registry entry referenced as: ALL_PROTOCOLS
const deepClone = ({ api, protocol, internalApiPromise }) => {
const ALL_PROTOCOLS = ({ api, protocol, internalApiPromise }) => {
api.idToken = async function () {
const { idToken: t } = (await internalApiPromise) || {};
const o = t
@@ -30,4 +30,4 @@ const deepClone = ({ api, protocol, internalApiPromise }) => {
};
};
const idTokenModule = deepClone;
const idTokenModule = ALL_PROTOCOLS;
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 Ut(o.intents.invoke(t, n));
return new parseUrl(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 = parseUrl(e);
const o = Dt(e);
const i = await internalApiPromise;
if (i && i.internalModal) {
await i.internalModal.show?.(t, o);
modules/resource-picker.js +1 -1
@@ -78,7 +78,7 @@ const resourcePickerModule = ({ api, protocol, internalApiPromise }) => {
signal: h,
},
);
subscribeAllErrors(
j(
protocol,
(t) => {
m();
modules/s-app-nav.js +1 -1
@@ -3,7 +3,7 @@
* Custom <s-app-nav> element for app navigation
*/
// Registry entry referenced as: Mn
// Registry entry referenced as: xn
modules/s-app-window.js +1 -1
@@ -3,9 +3,9 @@
* Custom <s-app-window> element for app window management
*/
// Registry entry referenced as: Ee
// Registry entry referenced as: ke
variantLock: 'app-window',
});
modules/scanner.js +2 -2
@@ -22,7 +22,7 @@ const scannerModule = ({ api, protocol, internalApiPromise }) => {
);
}
function s() {
subscribeAllErrors(protocol, a, {
j(protocol, a, {
signal: r,
id: o,
});
@@ -50,14 +50,14 @@ const scannerModule = ({ api, protocol, internalApiPromise }) => {
protocol.subscribe(
'getState',
({ features }) => {
s();
} else {
(function () {
const e = new AbortController();
abortController.signal.addEventListener('abort', () => e.abort());
subscribeAllErrors(
j(
protocol,
(t) => {
e.abort();
modules/share.js +1 -1
@@ -23,7 +23,7 @@ const shareModule = ({ protocol, internalApiPromise }) => {
}),
);
}
subscribeAllErrors(protocol, u, {
j(protocol, u, {
signal: c,
id: o,
});
modules/shortcut.js +1 -1
@@ -4,10 +4,10 @@
*/
const shortcutModule = ({ protocol, internalApiPromise }) => {
createDeferred({
N({
...e,
handler: async () => {
const { shortcut: o } = (await internalApiPromise) || {};
modules/telemetry.js +35 -24
@@ -4,31 +4,42 @@
*/
const telemetryModule = async ({ internalApiPromise }) => {
if (typeof window != 'undefined') {
async function n(n, e) {
window.addEventListener('_PreactCustomElement:connected', async (n) => {
const o = n.tagName.toLowerCase();
const e = n.target;
const i = (function (t) {
const o = e.tagName.toLowerCase();
const n = [];
const i = (function (t) {
for (let e = 0; e < t.attributes.length; e++) {
const n = [];
const o = t.attributes[e];
for (let e = 0; e < t.attributes.length; e++) {
n.push(o.name);
const o = t.attributes[e];
}
n.push(o.name);
return n;
}
})(n);
return n;
const r = JSON.stringify(i);
})(e);
const a = await internalApiPromise;
const r = JSON.stringify(i);
if (!a || !a.telemetry || typeof a.telemetry.increment != 'function') return;
const a = await internalApiPromise;
const s = globalThis.polaris?.version;
if (!a || !a.telemetry || typeof a.telemetry.increment != 'function') return;
const c = e?.reactMajorVersion;
const s = globalThis.polaris?.version;
a.telemetry.increment('ui-component', {
a.telemetry.increment('ui-component', {
component: o,
attributes: r,
...(s
...(s
? {
? {
version: s,
version: s,
}
}
: {}),
: {}),
...(typeof c == 'string'
});
? {
reactMajorVersion: c,
}
: {}),
});
}
if (typeof window != 'undefined') {
globalThis[xe] = (t, e) => {
if (t.getRootNode() === t.ownerDocument) {
n(t, e);
}
};
window.addEventListener(Re, (t) => n(t.target, t));
}
};
modules/title-bar.js +3 -3
@@ -22,18 +22,18 @@ const titleBarModule = ({ protocol, internalApiPromise }) => {
if (e) return e.getAttribute('title') ?? document.title;
}
function r(t) {
if (n) {
const e = `${ie}, ${re}, ${ae}`;
const e = `${re}, ${ae}, ${se}`;
if (o) return void o.click();
for (const n of i) {
if (e) return void e.click();
}
}
o?.click();
}
function a(t) {
@@ -67,11 +67,11 @@ const titleBarModule = ({ protocol, internalApiPromise }) => {
const n = [t];
const e = t.startsWith('s-internal-');
return [
...Dt('s-menu')
...SIMULATING_CLICK('s-menu')
.filter((t) => t.startsWith('s-internal-') === e)
.map((t) => t + '.title-bar-menu'),
].join(', ');
@@ -114,27 +114,27 @@ const titleBarModule = ({ protocol, internalApiPromise }) => {
};
}
} else {
hideElements: false,
context: document,
});
c(t && n?.localName === SIMULATING_CLICK ? SIMULATING_CLICK : undefined);
c(t && n?.localName === qt ? qt : undefined);
if (t) {
r = {
title: t.title || i(),
};
if (t.breadcrumb) {
}
if (t.primaryAction) {
r.buttons = Object.assign(r.buttons ?? {}, {
primary: n,
});
}
hideElements: false,
context: document,
});
modules/tools.js +1 -1
@@ -18,15 +18,15 @@ const toolsModule = async ({ api, internalApiPromise }) => {
(function (t) {
return async (...n) => {
(function () {
for (const t of set) t.capture();
for (const t of K) t.capture();
})();
try {
return await t(...n);
} finally {
if (typeof requestAnimationFrame == 'function') {
} else {
}
}
};
modules/ui-modal.js +1 -1
@@ -3,7 +3,7 @@
* Custom <ui-modal> element
*/
// Registry entry referenced as: Me
// Registry entry referenced as: _e
modules/ui-nav-menu.js +1 -1
@@ -3,7 +3,7 @@
* Custom <ui-nav-menu> element
*/
// Registry entry referenced as: xe
// Registry entry referenced as: Fe
modules/web-vitals.js +2 -2
@@ -33,14 +33,14 @@ const webVitalsModule = async ({ api, protocol, internalApiPromise, rpcEventTarg
id: o.id,
name: o.name,
value: o.value,
version: It,
version: SHOPIFY_PROTOCOLS,
});
} else {
protocol.send('WebVitals.' + t, {
id: o.id,
metricName: o.name,
value: o.value,
version: It,
version: SHOPIFY_PROTOCOLS,
});
}
if (l && o.name === 'LCP') {