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;
case "HeadContent":
var content = RemoveScripts(SiteState.Properties.HeadContent) + "\n";
var content = FormatScripts(SiteState.Properties.HeadContent) + "\n";
if (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");
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");
}
}

View File

@ -547,37 +547,20 @@
private string CreateScript(Resource resource, Alias alias)
{
if (!string.IsNullOrEmpty(resource.Url))
var src = resource.Url;
if (!string.IsNullOrEmpty(src))
{
if (!resource.Reload)
{
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>";
src = (resource.Url.Contains("://")) ? resource.Url : alias.BaseUrl + resource.Url;
}
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)

View File

@ -1,88 +1,170 @@
const pageScriptInfoBySrc = new Map();
function registerPageScriptElement(src) {
if (!src) {
throw new Error('Must provide a non-empty value for the "src" attribute.');
function getKey(element) {
if (element.src !== "") {
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) {
pageScriptInfo.referenceCount++;
} else {
pageScriptInfo = { referenceCount: 1, module: null };
pageScriptInfoBySrc.set(src, pageScriptInfo);
initializePageScriptModule(src, pageScriptInfo);
if (element.src.startsWith("./")) {
element.src = new URL(element.src.substr(2), document.baseURI).toString();
}
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) {
if (!src) {
return;
}
const pageScriptInfo = pageScriptInfoBySrc.get(src);
function unregisterPageScriptElement(element) {
const pageScriptInfo = pageScriptInfoBySrc.get(getKey(element));
if (!pageScriptInfo) {
return;
}
pageScriptInfo.referenceCount--;
}
async function initializePageScriptModule(src, pageScriptInfo) {
// If the path is relative, normalize it by by making it an absolute URL
// with document's the base HREF.
if (src.startsWith("./")) {
src = new URL(src.substr(2), document.baseURI).toString();
async function initializePageScript(pageScriptInfo) {
if (pageScriptInfo.reload) {
const module = await import(pageScriptInfo.src);
pageScriptInfo.module = module;
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() {
// Start by invoking 'onDispose' on any modules that are no longer referenced.
for (const [src, { module, referenceCount }] of pageScriptInfoBySrc) {
if (referenceCount <= 0) {
module?.onDispose?.();
pageScriptInfoBySrc.delete(src);
for (const [key, pageScriptInfo] of pageScriptInfoBySrc) {
if (pageScriptInfo.referenceCount <= 0) {
if (pageScriptInfo.module) {
pageScriptInfo.module.onDispose?.();
}
pageScriptInfoBySrc.delete(key);
}
}
// Then invoke 'onUpdate' on the remaining modules.
for (const { module } of pageScriptInfoBySrc.values()) {
module?.onUpdate?.();
for (const [key, pageScriptInfo] of pageScriptInfoBySrc) {
if (pageScriptInfo.module) {
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) {
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) {
if (name !== 'src') {
return;
switch (name) {
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;
unregisterPageScriptElement(oldValue);
registerPageScriptElement(newValue);
connectedCallback() {
registerPageScriptElement(this);
}
disconnectedCallback() {
unregisterPageScriptElement(this.src);
unregisterPageScriptElement(this);
}
});