diff --git a/.gitattributes b/.gitattributes index ce6b1cdc..f6db3d54 100644 --- a/.gitattributes +++ b/.gitattributes @@ -19,3 +19,6 @@ /infection.json5 export-ignore /.gitignore export-ignore /.gitattributes export-ignore + +# Vendored/minified frontend libraries: treat as binary to keep diffs readable +src/view/frontend/web/js/lib/alpine.min.js binary diff --git a/.github/workflows/functional-tests.yml b/.github/workflows/functional-tests.yml index 205c67b7..a8e51ff5 100644 --- a/.github/workflows/functional-tests.yml +++ b/.github/workflows/functional-tests.yml @@ -200,10 +200,10 @@ jobs: # Verify Alpine.js auto-loading in template echo "Verifying Alpine.js auto-loading for non-Hyvä themes:" - if grep -q "jsdelivr.net/npm/alpinejs" "$INSPECTOR_TPL"; then - echo "✓ Template includes Alpine.js CDN loading" + if grep -q "getAlpineJsUrl" "$INSPECTOR_TPL"; then + echo "✓ Template includes local Alpine.js fallback loading" else - echo "✗ Template missing Alpine.js auto-loading" + echo "✗ Template missing local Alpine.js fallback loading" exit 1 fi @@ -214,6 +214,13 @@ jobs: exit 1 fi + if grep -q "jsdelivr.net/npm/alpinejs" "$INSPECTOR_TPL"; then + echo "✗ Template still references external Alpine.js CDN" + exit 1 + else + echo "✓ Template no longer relies on external Alpine.js CDN" + fi + # Test Luma theme (non-Hyvä) echo "Testing Inspector with Luma theme (non-Hyvä):" bin/magento config:set design/theme/theme_id 4 # Luma theme ID diff --git a/src/Block/Inspector.php b/src/Block/Inspector.php index 7af90343..f2ef3306 100644 --- a/src/Block/Inspector.php +++ b/src/Block/Inspector.php @@ -101,6 +101,16 @@ public function getToolbarJsUrl(): string return $this->getViewFileUrl('OpenForgeProject_MageForge::js/toolbar.js'); } + /** + * Get local Alpine.js fallback URL + * + * @return string + */ + public function getAlpineJsUrl(): string + { + return $this->getViewFileUrl('OpenForgeProject_MageForge::js/lib/alpine.min.js'); + } + /** * Whether button labels should be displayed in the toolbar * diff --git a/src/etc/frontend/csp_whitelist.xml b/src/etc/frontend/csp_whitelist.xml deleted file mode 100644 index eb3a94f1..00000000 --- a/src/etc/frontend/csp_whitelist.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - cdn.jsdelivr.net - - - - diff --git a/src/view/frontend/templates/inspector.phtml b/src/view/frontend/templates/inspector.phtml index f7906bbc..532376a5 100644 --- a/src/view/frontend/templates/inspector.phtml +++ b/src/view/frontend/templates/inspector.phtml @@ -8,7 +8,8 @@ declare(strict_types=1); * Initializes the Alpine.js inspector component. * The floating button and info badge are created dynamically via JavaScript. * - * Note: Loads Alpine.js from CDN if not already available (for non-Hyvä themes like Luma) + * Note: Loads bundled Alpine.js locally as fallback if not already available + * (e.g. for non-Hyvä themes like Luma). No external CDN is used. * * @var \OpenForgeProject\MageForge\Block\Inspector $block */ @@ -17,31 +18,30 @@ declare(strict_types=1); - + getAlpineJsUrl(), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); $alpineBootstrap = << { + var ee = !1, + re = !1, + W = [], + ne = -1, + ie = !1; + function Ve(t) { + Dn(t); + } + function Ue() { + ie = !0; + } + function qe() { + ((ie = !1), We()); + } + function Dn(t) { + (W.includes(t) || W.push(t), We()); + } + function Ke(t) { + let e = W.indexOf(t); + e !== -1 && e > ne && W.splice(e, 1); + } + function We() { + if (!re && !ee) { + if (ie) return; + ((ee = !0), queueMicrotask(In)); + } + } + function In() { + ((ee = !1), (re = !0)); + for (let t = 0; t < W.length; t++) (W[t](), (ne = t)); + ((W.length = 0), (ne = -1), (re = !1)); + } + var C, + R, + j, + se, + oe = !0; + function Ge(t) { + ((oe = !1), t(), (oe = !0)); + } + function Je(t) { + ((C = t.reactive), + (j = t.release), + (R = (e) => + t.effect(e, { + scheduler: (r) => { + oe ? Ve(r) : r(); + }, + })), + (se = t.raw)); + } + function ae(t) { + R = t; + } + function Ye(t) { + let e = () => {}; + return [ + (n) => { + let i = R(n); + return ( + t._x_effects || + ((t._x_effects = new Set()), + (t._x_runEffects = () => { + t._x_effects.forEach((o) => o()); + })), + t._x_effects.add(i), + (e = () => { + i !== void 0 && (t._x_effects.delete(i), j(i)); + }), + i + ); + }, + () => { + e(); + }, + ]; + } + function St(t, e) { + let r = !0, + n, + i, + o = R(() => { + let s = t(), + a = JSON.stringify(s); + if (!r && (typeof s == "object" || s !== n)) { + let c = typeof n == "object" ? JSON.parse(i) : n; + queueMicrotask(() => { + e(s, c); + }); + } + ((n = s), (i = a), (r = !1)); + }); + return () => j(o); + } + async function Xe(t) { + Ue(); + try { + (await t(), await Promise.resolve()); + } finally { + qe(); + } + } + var Ze = [], + Qe = [], + tr = []; + function er(t) { + tr.push(t); + } + function et(t, e) { + typeof e == "function" + ? (t._x_cleanups || (t._x_cleanups = []), t._x_cleanups.push(e)) + : ((e = t), Qe.push(e)); + } + function At(t) { + Ze.push(t); + } + function Ot(t, e, r) { + (t._x_attributeCleanups || (t._x_attributeCleanups = {}), + t._x_attributeCleanups[e] || (t._x_attributeCleanups[e] = []), + t._x_attributeCleanups[e].push(r)); + } + function ce(t, e) { + t._x_attributeCleanups && + Object.entries(t._x_attributeCleanups).forEach(([r, n]) => { + (e === void 0 || e.includes(r)) && + (n.forEach((i) => i()), delete t._x_attributeCleanups[r]); + }); + } + function rr(t) { + for (t._x_effects?.forEach(Ke); t._x_cleanups?.length;) + t._x_cleanups.pop()(); + } + var le = new MutationObserver(pe), + ue = !1; + function ut() { + (le.observe(document, { + subtree: !0, + childList: !0, + attributes: !0, + attributeOldValue: !0, + }), + (ue = !0)); + } + function fe() { + (kn(), le.disconnect(), (ue = !1)); + } + var lt = []; + function kn() { + let t = le.takeRecords(); + lt.push(() => t.length > 0 && pe(t)); + let e = lt.length; + queueMicrotask(() => { + if (lt.length === e) for (; lt.length > 0;) lt.shift()(); + }); + } + function m(t) { + if (!ue) return t(); + fe(); + let e = t(); + return (ut(), e); + } + var de = !1, + vt = []; + function nr() { + de = !0; + } + function ir() { + ((de = !1), pe(vt), (vt = [])); + } + function pe(t) { + if (de) { + vt = vt.concat(t); + return; + } + let e = [], + r = new Set(), + n = new Map(), + i = new Map(); + for (let o = 0; o < t.length; o++) + if ( + !t[o].target._x_ignoreMutationObserver && + (t[o].type === "childList" && + (t[o].removedNodes.forEach((s) => { + s.nodeType === 1 && s._x_marker && r.add(s); + }), + t[o].addedNodes.forEach((s) => { + if (s.nodeType === 1) { + if (r.has(s)) { + r.delete(s); + return; + } + s._x_marker || e.push(s); + } + })), + t[o].type === "attributes") + ) { + let s = t[o].target, + a = t[o].attributeName, + c = t[o].oldValue, + l = () => { + (n.has(s) || n.set(s, []), + n.get(s).push({ name: a, value: s.getAttribute(a) })); + }, + u = () => { + (i.has(s) || i.set(s, []), i.get(s).push(a)); + }; + s.hasAttribute(a) && c === null + ? l() + : s.hasAttribute(a) + ? (u(), l()) + : u(); + } + (i.forEach((o, s) => { + ce(s, o); + }), + n.forEach((o, s) => { + Ze.forEach((a) => a(s, o)); + })); + for (let o of r) e.some((s) => s.contains(o)) || Qe.forEach((s) => s(o)); + for (let o of e) o.isConnected && tr.forEach((s) => s(o)); + ((e = null), (r = null), (n = null), (i = null)); + } + function Ct(t) { + return P(F(t)); + } + function N(t, e, r) { + return ( + (t._x_dataStack = [e, ...F(r || t)]), + () => { + t._x_dataStack = t._x_dataStack.filter((n) => n !== e); + } + ); + } + function F(t) { + return t._x_dataStack + ? t._x_dataStack + : typeof ShadowRoot == "function" && t instanceof ShadowRoot + ? F(t.host) + : t.parentNode + ? F(t.parentNode) + : []; + } + function P(t) { + return new Proxy({ objects: t }, $n); + } + function or(t, e) { + return t === null || t === Object.prototype + ? null + : Object.prototype.hasOwnProperty.call(t, e) + ? t + : or(Object.getPrototypeOf(t), e); + } + var $n = { + ownKeys({ objects: t }) { + return Array.from(new Set(t.flatMap((e) => Object.keys(e)))); + }, + has({ objects: t }, e) { + return e == Symbol.unscopables + ? !1 + : t.some( + (r) => + Object.prototype.hasOwnProperty.call(r, e) || Reflect.has(r, e), + ); + }, + get({ objects: t }, e, r) { + return e == "toJSON" + ? Ln + : Reflect.get(t.find((n) => Reflect.has(n, e)) || {}, e, r); + }, + set({ objects: t }, e, r, n) { + let i; + for (let s of t) if (((i = or(s, e)), i)) break; + i || (i = t[t.length - 1]); + let o = Object.getOwnPropertyDescriptor(i, e); + return o?.set && o?.get ? o.set.call(n, r) || !0 : Reflect.set(i, e, r); + }, + }; + function Ln() { + return Reflect.ownKeys(this).reduce( + (e, r) => ((e[r] = Reflect.get(this, r)), e), + {}, + ); + } + function rt(t) { + let e = (n) => typeof n == "object" && !Array.isArray(n) && n !== null, + r = (n, i = "") => { + Object.entries(Object.getOwnPropertyDescriptors(n)).forEach( + ([o, { value: s, enumerable: a }]) => { + if ( + a === !1 || + s === void 0 || + (typeof s == "object" && s !== null && s.__v_skip) + ) + return; + let c = i === "" ? o : `${i}.${o}`; + typeof s == "object" && s !== null && s._x_interceptor + ? (n[o] = s.initialize(t, c, o)) + : e(s) && s !== n && !(s instanceof Element) && r(s, c); + }, + ); + }; + return r(t); + } + function Tt(t, e = () => {}) { + let r = { + initialValue: void 0, + _x_interceptor: !0, + initialize(n, i, o) { + return t( + this.initialValue, + () => jn(n, i), + (s) => me(n, i, s), + i, + o, + ); + }, + }; + return ( + e(r), + (n) => { + if (typeof n == "object" && n !== null && n._x_interceptor) { + let i = r.initialize.bind(r); + r.initialize = (o, s, a) => { + let c = n.initialize(o, s, a); + return ((r.initialValue = c), i(o, s, a)); + }; + } else r.initialValue = n; + return r; + } + ); + } + function jn(t, e) { + return e.split(".").reduce((r, n) => r[n], t); + } + function me(t, e, r) { + if ((typeof e == "string" && (e = e.split(".")), e.length === 1)) + t[e[0]] = r; + else { + if (e.length === 0) throw error; + return (t[e[0]] || (t[e[0]] = {}), me(t[e[0]], e.slice(1), r)); + } + } + var sr = {}; + function x(t, e) { + sr[t] = e; + } + function H(t, e) { + let r = Fn(e); + return ( + Object.entries(sr).forEach(([n, i]) => { + Object.defineProperty(t, `$${n}`, { + get() { + return i(e, r); + }, + enumerable: !1, + }); + }), + t + ); + } + function Fn(t) { + let [e, r] = he(t), + n = { interceptor: Tt, ...e }; + return (et(t, r), n); + } + function ar(t, e, r, ...n) { + try { + return r(...n); + } catch (i) { + nt(i, t, e); + } + } + function nt(...t) { + return cr(...t); + } + var cr = Bn; + function lr(t) { + cr = t; + } + function Bn(t, e, r = void 0) { + ((t = Object.assign(t ?? { message: "No error message given." }, { + el: e, + expression: r, + })), + console.warn( + `Alpine Expression Error: ${t.message} + +${ + r + ? 'Expression: "' + + r + + `" + +` + : "" +}`, + e, + ), + setTimeout(() => { + throw t; + }, 0)); + } + var it = !0; + function Mt(t) { + let e = it; + it = !1; + let r = t(); + return ((it = e), r); + } + function T(t, e, r = {}) { + let n; + return (_(t, e)((i) => (n = i), r), n); + } + function _(...t) { + return ur(...t); + } + var ur = () => {}; + function fr(t) { + ur = t; + } + var dr; + function pr(t) { + dr = t; + } + function mr(t, e) { + let r = {}; + H(r, t); + let n = [r, ...F(t)], + i = typeof e == "function" ? zn(n, e) : Vn(n, e, t); + return ar.bind(null, t, e, i); + } + function zn(t, e) { + return ( + r = () => {}, + { scope: n = {}, params: i = [], context: o } = {}, + ) => { + if (!it) { + ft(r, e, P([n, ...t]), i); + return; + } + let s = e.apply(P([n, ...t]), i); + ft(r, s); + }; + } + var _e = {}; + function Hn(t, e) { + if (_e[t]) return _e[t]; + let r = Object.getPrototypeOf(async function () {}).constructor, + n = + /^[\n\s]*if.*\(.*\)/.test(t.trim()) || /^(let|const)\s/.test(t.trim()) + ? `(async()=>{ ${t} })()` + : t, + o = (() => { + try { + let s = new r( + ["__self", "scope"], + `with (scope) { __self.result = ${n} }; __self.finished = true; return __self.result;`, + ); + return ( + Object.defineProperty(s, "name", { value: `[Alpine] ${t}` }), + s + ); + } catch (s) { + return (nt(s, e, t), Promise.resolve()); + } + })(); + return ((_e[t] = o), o); + } + function Vn(t, e, r) { + let n = Hn(e, r); + return ( + i = () => {}, + { scope: o = {}, params: s = [], context: a } = {}, + ) => { + ((n.result = void 0), (n.finished = !1)); + let c = P([o, ...t]); + if (typeof n == "function") { + let l = n.call(a, n, c).catch((u) => nt(u, r, e)); + n.finished + ? (ft(i, n.result, c, s, r), (n.result = void 0)) + : l + .then((u) => { + ft(i, u, c, s, r); + }) + .catch((u) => nt(u, r, e)) + .finally(() => (n.result = void 0)); + } + }; + } + function ft(t, e, r, n, i) { + if (it && typeof e == "function") { + let o = e.apply(r, n); + o instanceof Promise + ? o.then((s) => ft(t, s, r, n)).catch((s) => nt(s, i, e)) + : t(o); + } else + typeof e == "object" && e instanceof Promise ? e.then((o) => t(o)) : t(e); + } + function hr(...t) { + return dr(...t); + } + function _r(t, e, r = {}) { + let n = {}; + H(n, t); + let i = [n, ...F(t)], + o = P([r.scope ?? {}, ...i]), + s = r.params ?? []; + if (e.includes("await")) { + let a = Object.getPrototypeOf(async function () {}).constructor, + c = + /^[\n\s]*if.*\(.*\)/.test(e.trim()) || /^(let|const)\s/.test(e.trim()) + ? `(async()=>{ ${e} })()` + : e; + return new a( + ["scope"], + `with (scope) { let __result = ${c}; return __result }`, + ).call(r.context, o); + } else { + let a = + /^[\n\s]*if.*\(.*\)/.test(e.trim()) || /^(let|const)\s/.test(e.trim()) + ? `(()=>{ ${e} })()` + : e, + l = new Function( + ["scope"], + `with (scope) { let __result = ${a}; return __result }`, + ).call(r.context, o); + return typeof l == "function" && it ? l.apply(o, s) : l; + } + } + var ye = "x-"; + function O(t = "") { + return ye + t; + } + function gr(t) { + ye = t; + } + var Rt = {}; + function p(t, e) { + return ( + (Rt[t] = e), + { + before(r) { + if (!Rt[r]) { + console.warn( + String.raw`Cannot find directive \`${r}\`. \`${t}\` will use the default order of execution`, + ); + return; + } + let n = G.indexOf(r); + G.splice(n >= 0 ? n : G.indexOf("DEFAULT"), 0, t); + }, + } + ); + } + function xr(t) { + return Object.keys(Rt).includes(t); + } + function pt(t, e, r) { + if (((e = Array.from(e)), t._x_virtualDirectives)) { + let o = Object.entries(t._x_virtualDirectives).map(([a, c]) => ({ + name: a, + value: c, + })), + s = be(o); + ((o = o.map((a) => + s.find((c) => c.name === a.name) + ? { name: `x-bind:${a.name}`, value: `"${a.value}"` } + : a, + )), + (e = e.concat(o))); + } + let n = {}; + return e + .map(wr((o, s) => (n[o] = s))) + .filter(Sr) + .map(qn(n, r)) + .sort(Kn) + .map((o) => Un(t, o)); + } + function be(t) { + return Array.from(t) + .map(wr()) + .filter((e) => !Sr(e)); + } + var ge = !1, + dt = new Map(), + yr = Symbol(); + function br(t) { + ge = !0; + let e = Symbol(); + ((yr = e), dt.set(e, [])); + let r = () => { + for (; dt.get(e).length;) dt.get(e).shift()(); + dt.delete(e); + }, + n = () => { + ((ge = !1), r()); + }; + (t(r), n()); + } + function he(t) { + let e = [], + r = (a) => e.push(a), + [n, i] = Ye(t); + return ( + e.push(i), + [ + { + Alpine: B, + effect: n, + cleanup: r, + evaluateLater: _.bind(_, t), + evaluate: T.bind(T, t), + }, + () => e.forEach((a) => a()), + ] + ); + } + function Un(t, e) { + let r = () => {}, + n = Rt[e.type] || r, + [i, o] = he(t); + Ot(t, e.original, o); + let s = () => { + t._x_ignore || + t._x_ignoreSelf || + (n.inline && n.inline(t, e, i), + (n = n.bind(n, t, e, i)), + ge ? dt.get(yr).push(n) : n()); + }; + return ((s.runCleanups = o), s); + } + var Nt = + (t, e) => + ({ name: r, value: n }) => ( + r.startsWith(t) && (r = r.replace(t, e)), + { name: r, value: n } + ), + Pt = (t) => t; + function wr(t = () => {}) { + return ({ name: e, value: r }) => { + let { name: n, value: i } = Er.reduce((o, s) => s(o), { + name: e, + value: r, + }); + return (n !== e && t(n, e), { name: n, value: i }); + }; + } + var Er = []; + function ot(t) { + Er.push(t); + } + function Sr({ name: t }) { + return vr().test(t); + } + var vr = () => new RegExp(`^${ye}([^:^.]+)\\b`); + function qn(t, e) { + return ({ name: r, value: n }) => { + r === n && (n = ""); + let i = r.match(vr()), + o = r.match(/:([a-zA-Z0-9\-_:]+)/), + s = r.match(/\.[^.\]]+(?=[^\]]*$)/g) || [], + a = e || t[r] || r; + return { + type: i ? i[1] : null, + value: o ? o[1] : null, + modifiers: s.map((c) => c.replace(".", "")), + expression: n, + original: a, + }; + }; + } + var xe = "DEFAULT", + G = [ + "ignore", + "ref", + "data", + "id", + "anchor", + "bind", + "init", + "for", + "model", + "modelable", + "transition", + "show", + "if", + xe, + "teleport", + ]; + function Kn(t, e) { + let r = G.indexOf(t.type) === -1 ? xe : t.type, + n = G.indexOf(e.type) === -1 ? xe : e.type; + return G.indexOf(r) - G.indexOf(n); + } + function J(t, e, r = {}, n = {}) { + return t.dispatchEvent( + new CustomEvent(e, { + detail: r, + bubbles: !0, + composed: !0, + cancelable: !0, + ...n, + }), + ); + } + function D(t, e) { + if (typeof ShadowRoot == "function" && t instanceof ShadowRoot) { + Array.from(t.children).forEach((i) => D(i, e)); + return; + } + let r = !1; + if ((e(t, () => (r = !0)), r)) return; + let n = t.firstElementChild; + for (; n;) (D(n, e, !1), (n = n.nextElementSibling)); + } + function E(t, ...e) { + console.warn(`Alpine Warning: ${t}`, ...e); + } + var Ar = !1; + function Or() { + (Ar && + E( + "Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems.", + ), + (Ar = !0), + document.body || + E( + "Unable to initialize. Trying to load Alpine before `` is available. Did you forget to add `defer` in Alpine's `