Merge pull request #4913 from sbwalker/dev

page-script enhancements
This commit is contained in:
Shaun Walker 2024-12-13 16:46:23 -05:00 committed by GitHub
commit 0296230219
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 163 additions and 82 deletions

View File

@ -41,7 +41,7 @@
} }
break; break;
case "HeadContent": case "HeadContent":
var content = RemoveScripts(SiteState.Properties.HeadContent) + "\n"; var content = FormatScripts(SiteState.Properties.HeadContent) + "\n";
if (content != _content) if (content != _content)
{ {
_content = content; _content = content;
@ -51,14 +51,30 @@
} }
} }
private string RemoveScripts(string headcontent) private string FormatScripts(string headcontent)
{ {
if (!string.IsNullOrEmpty(headcontent) && RenderMode == RenderModes.Interactive) if (!string.IsNullOrEmpty(headcontent))
{ {
var index = headcontent.IndexOf("<script"); var index = headcontent.IndexOf("<script");
while (index >= 0) while (index >= 0)
{ {
headcontent = headcontent.Remove(index, headcontent.IndexOf("</script>") + 9 - index); var script = headcontent.Substring(index, headcontent.IndexOf("</script>") + 9 - index);
if (RenderMode == RenderModes.Interactive)
{
// remove scripts when interactive rendering
headcontent = headcontent.Remove(index, script.Length);
}
else
{
// transform scripts into page-scripts when static rendering
var pagescript = script.Replace("script", "page-script");
if (!pagescript.Contains("><"))
{
var content = pagescript.Substring(pagescript.IndexOf(">") + 1, pagescript.LastIndexOf("<") - pagescript.IndexOf(">") - 1);
pagescript = pagescript.Replace(">" + content, " content=\"" + content.Replace("\"","'") + "\">");
}
headcontent = headcontent.Replace(script, pagescript);
}
index = headcontent.IndexOf("<script"); index = headcontent.IndexOf("<script");
} }
} }

View File

@ -547,37 +547,20 @@
private string CreateScript(Resource resource, Alias alias) private string CreateScript(Resource resource, Alias alias)
{ {
if (!string.IsNullOrEmpty(resource.Url)) var src = resource.Url;
if (!string.IsNullOrEmpty(src))
{ {
if (!resource.Reload) src = (resource.Url.Contains("://")) ? resource.Url : alias.BaseUrl + resource.Url;
{
var url = (resource.Url.Contains("://")) ? resource.Url : alias.BaseUrl + resource.Url;
return "<script" +
((!string.IsNullOrEmpty(resource.Integrity)) ? " integrity=\"" + resource.Integrity + "\"" : "") +
((!string.IsNullOrEmpty(resource.CrossOrigin)) ? " crossorigin=\"" + resource.CrossOrigin + "\"" : "") +
((resource.ES6Module) ? " type=\"module\"" : "") +
" src=\"" + url + "\"></script>"; // src at end of element due to enhanced navigation patch algorithm
}
else
{
// use custom element which can execute script on every page transition
@if (string.IsNullOrEmpty(resource.Integrity) && string.IsNullOrEmpty(resource.CrossOrigin))
{
return "<page-script src=\"" + resource.Url + "\"></page-script>";
}
else
{
// use modulepreload for external resources
return "<link rel=\"modulepreload\" href=\"" + resource.Url + "\" integrity=\"" + resource.Integrity + "\" crossorigin=\"" + resource.CrossOrigin + "\" />\n" +
"<page-script src=\"" + resource.Url + "\"></page-script>";
}
}
}
else
{
// inline script
return "<script>" + resource.Content + "</script>";
} }
return "<page-script" +
((!string.IsNullOrEmpty(src)) ? " src=\"" + src + "\"" : "") +
((resource.ES6Module || resource.Reload) ? " type=\"module\"" : "") +
((!string.IsNullOrEmpty(resource.Integrity)) ? " integrity=\"" + resource.Integrity + "\"" : "") +
((!string.IsNullOrEmpty(resource.CrossOrigin)) ? " crossorigin=\"" + resource.CrossOrigin + "\"" : "") +
((!string.IsNullOrEmpty(resource.Content)) ? " content=\"" + resource.Content + "\"" : "") +
((resource.Reload) ? " reload=\"true\"" : "") +
"></page-script>";
} }
private void SetLocalizationCookie(string cookieValue) private void SetLocalizationCookie(string cookieValue)

View File

@ -1,88 +1,170 @@
const pageScriptInfoBySrc = new Map(); const pageScriptInfoBySrc = new Map();
function registerPageScriptElement(src) { function getKey(element) {
if (!src) { if (element.src !== "") {
throw new Error('Must provide a non-empty value for the "src" attribute.'); return element.src;
} else {
return element.content;
} }
}
let pageScriptInfo = pageScriptInfoBySrc.get(src); function registerPageScriptElement(element) {
let key = getKey(element);
let pageScriptInfo = pageScriptInfoBySrc.get(key);
if (pageScriptInfo) { if (pageScriptInfo) {
pageScriptInfo.referenceCount++; pageScriptInfo.referenceCount++;
} else { } else {
pageScriptInfo = { referenceCount: 1, module: null }; if (element.src.startsWith("./")) {
pageScriptInfoBySrc.set(src, pageScriptInfo); element.src = new URL(element.src.substr(2), document.baseURI).toString();
initializePageScriptModule(src, pageScriptInfo); }
pageScriptInfo = { src: element.src, type: element.type, integrity: element.integrity, crossorigin: element.crossorigin, content: element.content, location: element.location, reload: element.reload, module: null, referenceCount: 1 };
pageScriptInfoBySrc.set(key, pageScriptInfo);
initializePageScript(pageScriptInfo);
} }
} }
function unregisterPageScriptElement(src) { function unregisterPageScriptElement(element) {
if (!src) { const pageScriptInfo = pageScriptInfoBySrc.get(getKey(element));
return;
}
const pageScriptInfo = pageScriptInfoBySrc.get(src);
if (!pageScriptInfo) { if (!pageScriptInfo) {
return; return;
} }
pageScriptInfo.referenceCount--; pageScriptInfo.referenceCount--;
} }
async function initializePageScriptModule(src, pageScriptInfo) { async function initializePageScript(pageScriptInfo) {
// If the path is relative, normalize it by by making it an absolute URL if (pageScriptInfo.reload) {
// with document's the base HREF. const module = await import(pageScriptInfo.src);
if (src.startsWith("./")) { pageScriptInfo.module = module;
src = new URL(src.substr(2), document.baseURI).toString(); module.onLoad?.();
module.onUpdate?.();
} else {
try {
injectScript(pageScriptInfo);
} catch (error) {
console.error("Failed to load script: ${pageScriptInfo.src}", error);
}
} }
const module = await import(src);
if (pageScriptInfo.referenceCount <= 0) {
// All page-script elements with the same 'src' were
// unregistered while we were loading the module.
return;
}
pageScriptInfo.module = module;
module.onLoad?.();
module.onUpdate?.();
} }
function onEnhancedLoad() { function onEnhancedLoad() {
// Start by invoking 'onDispose' on any modules that are no longer referenced. for (const [key, pageScriptInfo] of pageScriptInfoBySrc) {
for (const [src, { module, referenceCount }] of pageScriptInfoBySrc) { if (pageScriptInfo.referenceCount <= 0) {
if (referenceCount <= 0) { if (pageScriptInfo.module) {
module?.onDispose?.(); pageScriptInfo.module.onDispose?.();
pageScriptInfoBySrc.delete(src); }
pageScriptInfoBySrc.delete(key);
} }
} }
// Then invoke 'onUpdate' on the remaining modules. for (const [key, pageScriptInfo] of pageScriptInfoBySrc) {
for (const { module } of pageScriptInfoBySrc.values()) { if (pageScriptInfo.module) {
module?.onUpdate?.(); pageScriptInfo.module.onUpdate?.();
} else {
try {
injectScript(pageScriptInfo);
} catch (error) {
if (pageScriptInfo.src !== "") {
console.error("Failed to load script library: ${pageScriptInfo.src}", error);
} else {
console.error("Failed to load inline script: ${pageScriptInfo.content}", error);
}
}
}
} }
} }
function injectScript(pageScriptInfo) {
return new Promise((resolve, reject) => {
var pageScript;
var script = document.createElement("script");
script.async = false;
if (pageScriptInfo.src !== "") {
pageScript = document.querySelector("page-script[src=\"" + pageScriptInfo.src + "\"]");
script.src = pageScriptInfo.src;
if (pageScriptInfo.type !== "") {
script.type = pageScriptInfo.type;
}
if (pageScriptInfo.integrity !== "") {
script.integrity = pageScriptInfo.integrity;
}
if (pageScriptInfo.crossorigin !== "") {
script.crossOrigin = pageScriptInfo.crossorigin;
}
} else {
pageScript = document.querySelector("page-script[content=\"" + CSS.escape(pageScriptInfo.content) + "\"]");
script.innerHTML = pageScriptInfo.content;
}
script.onload = () => resolve();
script.onerror = (error) => reject(error);
// add script to page
if (pageScriptInfo.location === "head") {
document.head.appendChild(script);
} else {
document.body.appendChild(script);
// note this throws an exception when page-script is on a page which has interactive components (ie Counter.razor)
// Error: Uncaught (in promise) TypeError: can't access property "attributes" of null
// this seems to be related to Blazor-Server-Component-State which is also injected at the end of the body
}
// remove page-script element from page
if (pageScript !== null) {
pageScript.remove();
}
});
}
export function afterWebStarted(blazor) { export function afterWebStarted(blazor) {
customElements.define('page-script', class extends HTMLElement { customElements.define('page-script', class extends HTMLElement {
static observedAttributes = ['src']; static observedAttributes = ['src', 'type', 'integrity', 'crossorigin', 'content', 'location', 'reload'];
constructor() {
super();
this.src = "";
this.type = "";
this.integrity = "";
this.crossorigin = "";
this.content = "";
this.location = "head";
this.reload = false;
}
// We use attributeChangedCallback instead of connectedCallback
// because a page-script element might get reused between enhanced
// navigations.
attributeChangedCallback(name, oldValue, newValue) { attributeChangedCallback(name, oldValue, newValue) {
if (name !== 'src') { switch (name) {
return; case "src":
this.src = newValue;
break;
case "type":
this.type = newValue;
break;
case "integrity":
this.integrity = newValue;
break;
case "crossorigin":
this.crossorigin = newValue;
break;
case "content":
this.content = newValue;
break;
case "location":
this.location = newValue;
break;
case "reload":
this.reload = newValue;
break;
} }
}
this.src = newValue; connectedCallback() {
unregisterPageScriptElement(oldValue); registerPageScriptElement(this);
registerPageScriptElement(newValue);
} }
disconnectedCallback() { disconnectedCallback() {
unregisterPageScriptElement(this.src); unregisterPageScriptElement(this);
} }
}); });