From fca33568e1aa9193967bb6b4287d066d56df3cb2 Mon Sep 17 00:00:00 2001 From: badassscoder Date: Wed, 4 Mar 2026 15:05:06 +0000 Subject: [PATCH 1/3] perf(startup): lazy-load ABCJS library to reduce startup parsing --- js/activity.js | 30 +++++++++++++++++++++++++++++- js/blocks/GraphicsBlocks.js | 9 ++++++--- js/utils/abcLoader.js | 22 ++++++++++++++++++++++ js/widgets/aiwidget.js | 26 ++++++++++++++++++++++++-- 4 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 js/utils/abcLoader.js diff --git a/js/activity.js b/js/activity.js index 3122474ede..e13798c6a9 100644 --- a/js/activity.js +++ b/js/activity.js @@ -48,6 +48,9 @@ Activity, LEADING, _THIS_IS_MUSIC_BLOCKS_, _THIS_IS_TURTLE_BLOCKS_, globalActivity, hideArrows, doAnalyzeProject */ + +import { ensureABCJS } from "./utils/abcLoader.js"; + const LEADING = 0; const BLOCKSCALES = [1, 1.5, 2, 3, 4]; const _THIS_IS_MUSIC_BLOCKS_ = true; @@ -200,6 +203,25 @@ if (_THIS_IS_MUSIC_BLOCKS_) { // instead of reaching through window.* globals. let globalActivity; +/** + * Ensures the ABCJS library is loaded. + * If already loaded, resolves immediately. + * Otherwise, dynamically appends the script and resolves on load. + * @returns {Promise} + */ +function ensureABCJS() { + if (typeof window !== "undefined" && window.ABCJS) return Promise.resolve(); + if (typeof window === "undefined") return Promise.resolve(); + + return new Promise((resolve, reject) => { + const script = document.createElement("script"); + script.src = "lib/abc.min.js"; + script.onload = resolve; + script.onerror = reject; + document.head.appendChild(script); + }); +} + /** * Performs analysis on the project using the global activity. * @returns {object} - The analysis result. @@ -7719,11 +7741,12 @@ class Activity { }; // Music Block Parser from abc to MB - abcReader.onload = event => { + abcReader.onload = async event => { //get the abc data and replace the / so that the block does not break let abcData = event.target.result; abcData = abcData.replace(/\\/g, ""); + await ensureABCJS(); const tunebook = new ABCJS.parseOnly(abcData); // eslint-disable-next-line no-console console.log(tunebook); @@ -8273,3 +8296,8 @@ define(["domReady!"].concat(MYDEFINES), doc => { }; initialize(); }); + +// Export Activity for Node/Jest tests +if (typeof module !== "undefined" && module.exports) { + module.exports = Activity; +} diff --git a/js/blocks/GraphicsBlocks.js b/js/blocks/GraphicsBlocks.js index 7475be7d1b..f53c21fa59 100644 --- a/js/blocks/GraphicsBlocks.js +++ b/js/blocks/GraphicsBlocks.js @@ -97,7 +97,8 @@ function setupGraphicsBlocks(activity) { * @returns {number} - The heading value. */ arg(logo, turtle, blk) { - const connections = activity.blocks.blockList[blk]?.connections; + const blockList = activity?.blocks?.blockList || []; + const connections = blockList[blk]?.connections; const parentId = connections?.[0]; if ( logo.inStatusMatrix && @@ -182,7 +183,8 @@ function setupGraphicsBlocks(activity) { * @returns {number} - The Y-coordinate value. */ arg(logo, turtle, blk) { - const connections = activity.blocks.blockList[blk]?.connections; + const blockList = activity?.blocks?.blockList || []; + const connections = blockList[blk]?.connections; const parentId = connections?.[0]; if ( logo.inStatusMatrix && @@ -268,7 +270,8 @@ function setupGraphicsBlocks(activity) { * @returns {number} - The X-coordinate value. */ arg(logo, turtle, blk) { - const connections = activity.blocks.blockList[blk]?.connections; + const blockList = activity?.blocks?.blockList || []; + const connections = blockList[blk]?.connections; const parentId = connections?.[0]; if ( logo.inStatusMatrix && diff --git a/js/utils/abcLoader.js b/js/utils/abcLoader.js new file mode 100644 index 0000000000..3e0d7f1448 --- /dev/null +++ b/js/utils/abcLoader.js @@ -0,0 +1,22 @@ +export async function ensureABCJS() { + if (typeof window !== "undefined" && window.ABCJS) return; + if (typeof window === "undefined") return; + + return new Promise((resolve, reject) => { + const existing = document.querySelector('script[src="lib/abc.min.js"]'); + if (existing) { + if (existing.hasAttribute('data-loaded')) resolve(); + else existing.addEventListener('load', resolve); + return; + } + + const script = document.createElement("script"); + script.src = "lib/abc.min.js"; + script.onload = () => { + script.setAttribute('data-loaded', 'true'); + resolve(); + }; + script.onerror = reject; + document.head.appendChild(script); + }); +} diff --git a/js/widgets/aiwidget.js b/js/widgets/aiwidget.js index b5ab354b7c..2176476bfd 100644 --- a/js/widgets/aiwidget.js +++ b/js/widgets/aiwidget.js @@ -18,6 +18,8 @@ */ /* exported Abhijeet Singh */ +import { ensureABCJS } from "../utils/abcLoader.js"; + /** * Represents a AI Widget. * @constructor @@ -28,6 +30,24 @@ function AIWidget() { const SAMPLEHEIGHT = 400; // Don't include natural when construcing the note name... const EXPORTACCIDENTALNAMES = [DOUBLEFLAT, FLAT, "", SHARP, DOUBLESHARP]; + + /** + * Ensures the ABCJS library is loaded. + * Dynamically appends the script if not already present. + * @returns {Promise} + */ + function ensureABCJS() { + if (typeof window !== "undefined" && window.ABCJS) return Promise.resolve(); + if (typeof window === "undefined") return Promise.resolve(); + + return new Promise((resolve, reject) => { + const script = document.createElement("script"); + script.src = "lib/abc.min.js"; + script.onload = resolve; + script.onerror = reject; + document.head.appendChild(script); + }); + } // ...but display it in the selector. const ACCIDENTALNAMES = [DOUBLEFLAT, FLAT, NATURAL, SHARP, DOUBLESHARP]; const SOLFEGENAMES = ["do", "re", "mi", "fa", "sol", "la", "ti", "do"]; @@ -720,7 +740,8 @@ function AIWidget() { * @private * @returns {void} */ - this.__save = function () { + this.__save = async function () { + await ensureABCJS(); const tunebook = new ABCJS.parseOnly(abcNotationSong); tunebook.forEach(tune => { @@ -873,7 +894,8 @@ function AIWidget() { * Plays the reference pitch based on the current sample's pitch, accidental, and octave. * @returns {void} */ - this._playABCSong = function () { + this._playABCSong = async function () { + await ensureABCJS(); const abc = abcNotationSong; const stopAudioButton = document.querySelector(".stop-audio"); From affa35fa41fa827a9e02a2d90fe9984fdf9dd348 Mon Sep 17 00:00:00 2001 From: badassscoder Date: Wed, 4 Mar 2026 16:33:43 +0000 Subject: [PATCH 2/3] chore: trigger CI rerun From c36619824c37955d25c3df36215a789d85395330 Mon Sep 17 00:00:00 2001 From: badassscoder Date: Mon, 9 Mar 2026 07:48:14 +0000 Subject: [PATCH 3/3] refactor: centralize ensureABCJS loader and fix tests --- js/activity.js | 21 --------------------- js/blocks/GraphicsBlocks.js | 9 +++------ js/utils/abcLoader.js | 32 ++++++++++++++++++++++++++------ js/widgets/aiwidget.js | 20 +------------------- 4 files changed, 30 insertions(+), 52 deletions(-) diff --git a/js/activity.js b/js/activity.js index e13798c6a9..52cf1afd31 100644 --- a/js/activity.js +++ b/js/activity.js @@ -49,8 +49,6 @@ globalActivity, hideArrows, doAnalyzeProject */ -import { ensureABCJS } from "./utils/abcLoader.js"; - const LEADING = 0; const BLOCKSCALES = [1, 1.5, 2, 3, 4]; const _THIS_IS_MUSIC_BLOCKS_ = true; @@ -203,25 +201,6 @@ if (_THIS_IS_MUSIC_BLOCKS_) { // instead of reaching through window.* globals. let globalActivity; -/** - * Ensures the ABCJS library is loaded. - * If already loaded, resolves immediately. - * Otherwise, dynamically appends the script and resolves on load. - * @returns {Promise} - */ -function ensureABCJS() { - if (typeof window !== "undefined" && window.ABCJS) return Promise.resolve(); - if (typeof window === "undefined") return Promise.resolve(); - - return new Promise((resolve, reject) => { - const script = document.createElement("script"); - script.src = "lib/abc.min.js"; - script.onload = resolve; - script.onerror = reject; - document.head.appendChild(script); - }); -} - /** * Performs analysis on the project using the global activity. * @returns {object} - The analysis result. diff --git a/js/blocks/GraphicsBlocks.js b/js/blocks/GraphicsBlocks.js index f53c21fa59..7475be7d1b 100644 --- a/js/blocks/GraphicsBlocks.js +++ b/js/blocks/GraphicsBlocks.js @@ -97,8 +97,7 @@ function setupGraphicsBlocks(activity) { * @returns {number} - The heading value. */ arg(logo, turtle, blk) { - const blockList = activity?.blocks?.blockList || []; - const connections = blockList[blk]?.connections; + const connections = activity.blocks.blockList[blk]?.connections; const parentId = connections?.[0]; if ( logo.inStatusMatrix && @@ -183,8 +182,7 @@ function setupGraphicsBlocks(activity) { * @returns {number} - The Y-coordinate value. */ arg(logo, turtle, blk) { - const blockList = activity?.blocks?.blockList || []; - const connections = blockList[blk]?.connections; + const connections = activity.blocks.blockList[blk]?.connections; const parentId = connections?.[0]; if ( logo.inStatusMatrix && @@ -270,8 +268,7 @@ function setupGraphicsBlocks(activity) { * @returns {number} - The X-coordinate value. */ arg(logo, turtle, blk) { - const blockList = activity?.blocks?.blockList || []; - const connections = blockList[blk]?.connections; + const connections = activity.blocks.blockList[blk]?.connections; const parentId = connections?.[0]; if ( logo.inStatusMatrix && diff --git a/js/utils/abcLoader.js b/js/utils/abcLoader.js index 3e0d7f1448..2868f8bff0 100644 --- a/js/utils/abcLoader.js +++ b/js/utils/abcLoader.js @@ -1,22 +1,42 @@ -export async function ensureABCJS() { - if (typeof window !== "undefined" && window.ABCJS) return; - if (typeof window === "undefined") return; +function ensureABCJS() { + if (typeof window !== "undefined" && window.ABCJS) { + return Promise.resolve(); + } + + if (typeof window === "undefined") { + return Promise.resolve(); + } return new Promise((resolve, reject) => { const existing = document.querySelector('script[src="lib/abc.min.js"]'); + if (existing) { - if (existing.hasAttribute('data-loaded')) resolve(); - else existing.addEventListener('load', resolve); + if (existing.hasAttribute("data-loaded")) { + resolve(); + } else { + existing.addEventListener("load", resolve); + } return; } const script = document.createElement("script"); script.src = "lib/abc.min.js"; + script.onload = () => { - script.setAttribute('data-loaded', 'true'); + script.setAttribute("data-loaded", "true"); resolve(); }; + script.onerror = reject; + document.head.appendChild(script); }); } + +if (typeof window !== "undefined") { + window.ensureABCJS = ensureABCJS; +} + +if (typeof module !== "undefined" && module.exports) { + module.exports = ensureABCJS; +} diff --git a/js/widgets/aiwidget.js b/js/widgets/aiwidget.js index 2176476bfd..99e2feb76b 100644 --- a/js/widgets/aiwidget.js +++ b/js/widgets/aiwidget.js @@ -18,7 +18,6 @@ */ /* exported Abhijeet Singh */ -import { ensureABCJS } from "../utils/abcLoader.js"; /** * Represents a AI Widget. @@ -31,23 +30,6 @@ function AIWidget() { // Don't include natural when construcing the note name... const EXPORTACCIDENTALNAMES = [DOUBLEFLAT, FLAT, "", SHARP, DOUBLESHARP]; - /** - * Ensures the ABCJS library is loaded. - * Dynamically appends the script if not already present. - * @returns {Promise} - */ - function ensureABCJS() { - if (typeof window !== "undefined" && window.ABCJS) return Promise.resolve(); - if (typeof window === "undefined") return Promise.resolve(); - - return new Promise((resolve, reject) => { - const script = document.createElement("script"); - script.src = "lib/abc.min.js"; - script.onload = resolve; - script.onerror = reject; - document.head.appendChild(script); - }); - } // ...but display it in the selector. const ACCIDENTALNAMES = [DOUBLEFLAT, FLAT, NATURAL, SHARP, DOUBLESHARP]; const SOLFEGENAMES = ["do", "re", "mi", "fa", "sol", "la", "ti", "do"]; @@ -895,7 +877,7 @@ function AIWidget() { * @returns {void} */ this._playABCSong = async function () { - await ensureABCJS(); + await window.ensureABCJS(); const abc = abcNotationSong; const stopAudioButton = document.querySelector(".stop-audio");