Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/controllers/AppController.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ public function actionHealthCheck(): Response
*
* @param string $url
* @return Response
* @deprecated in 4.18.0. Appended CP resources no longer rely on this proxy.
*/
public function actionResourceJs(string $url): Response
{
Expand Down
2 changes: 1 addition & 1 deletion src/web/assets/cp/dist/cp.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/web/assets/cp/dist/cp.js.map

Large diffs are not rendered by default.

99 changes: 67 additions & 32 deletions src/web/assets/cp/src/js/Craft.js
Original file line number Diff line number Diff line change
Expand Up @@ -1812,56 +1812,91 @@ $.extend(Craft, {

_existingCss: null,
_existingJs: null,
_appendHtmlQueue: null,

_appendHtml: function (html, $parent) {
_appendHtml: async function (html, $parent) {
if (!html) {
return;
}

const nodes = $.parseHTML(html.trim(), true).filter((node) => {
/**
* Separate scripts from other nodes to bypass jQuery's internal
* script handling, which uses sync XHR (jQuery._evalUrl) and
* silently falls back to async for cross-origin URLs, breaking
* execution order.
*
* @see https://github.com/jquery/jquery/issues/4801
* @see https://github.com/jquery/jquery/issues/1895
*/
const scriptNodes = [];
const otherNodes = [];

for (const node of $.parseHTML(html.trim(), true)) {
// Deduplicate CSS
if (node.nodeName === 'LINK' && node.href) {
if (!this._existingCss) {
this._existingCss = $('link[href]')
.toArray()
.map((n) => n.href.replace(/&/g, '&'));
}

if (this._existingCss.includes(node.href)) {
return false;
continue;
}

this._existingCss.push(node.href);
return true;
}

if (node.nodeName === 'SCRIPT' && node.src) {
if (!this._existingJs) {
this._existingJs = $('script[src]')
.toArray()
.map((n) => n.src.replace(/&/g, '&'));
// Deduplicate and separate scripts
if (node.nodeName === 'SCRIPT') {
if (node.src) {
if (!this._existingJs) {
this._existingJs = $('script[src]')
.toArray()
.map((n) => n.src.replace(/&/g, '&'));
}
if (this._existingJs.includes(node.src)) {
continue;
}
this._existingJs.push(node.src);
}
scriptNodes.push(node);
continue;
}

// if this is a cross-domain JS resource, use our app/resource-js proxy to load it
if (
node.src.startsWith(this.resourceBaseUrl) &&
!this.isSameHost(node.src)
) {
node.src = this.getActionUrl('app/resource-js', {
url: node.src,
});
}
otherNodes.push(node);
}

if (this._existingJs.includes(node.src)) {
return false;
}
if (otherNodes.length) {
$parent.append(otherNodes);
}

// Load scripts sequentially via native <script> insertion to
// preserve execution order without requiring CORS.
const parentEl = $parent[0];
for (const scriptNode of scriptNodes) {
const scriptEl = document.createElement('script');

this._existingJs.push(node.src);
for (const attr of scriptNode.attributes) {
scriptEl.setAttribute(attr.name, attr.value);
}

return true;
});
if (scriptEl.src) {
await new Promise((resolve) => {
scriptEl.onload = scriptEl.onerror = resolve;
parentEl.appendChild(scriptEl);
});
} else {
scriptEl.textContent = scriptNode.textContent;
parentEl.appendChild(scriptEl);
}
}
},

$parent.append(nodes);
_queueAppendHtml: function (html, $parent) {
const append = () => this._appendHtml(html, $parent);
this._appendHtmlQueue = (this._appendHtmlQueue || Promise.resolve())
.catch(() => {})
.then(append);
return this._appendHtmlQueue;
},

/**
Expand All @@ -1870,8 +1905,8 @@ $.extend(Craft, {
* @param {string} html
* @returns {Promise}
*/
appendHeadHtml: async function (html) {
this._appendHtml(html, $('head'));
appendHeadHtml: function (html) {
return this._queueAppendHtml(html, $('head'));
},

/**
Expand All @@ -1880,8 +1915,8 @@ $.extend(Craft, {
* @param {string} html
* @returns {Promise}
*/
appendBodyHtml: async function (html) {
this._appendHtml(html, Garnish.$bod);
appendBodyHtml: function (html) {
return this._queueAppendHtml(html, Garnish.$bod);
},

/**
Expand All @@ -1893,7 +1928,7 @@ $.extend(Craft, {
console.warn(
'Craft.appendFootHtml() is deprecated. Craft.appendBodyHtml() should be used instead.'
);
this.appendBodyHtml(html);
return this.appendBodyHtml(html);
},

/**
Expand Down
Loading