vercengen/startup/vercengen_startup.js

//Import Node.js libraries
global.fs = require("fs");
global.path = require("path");

//Initialise functions
{
	/**
	 * @namespace global
	 */
	if (!global) global = {};
	if (!global.ve)
		/**
		 * The root namespace for all Vercengen classes and options.
		 * 
		 * @namespace ve
		 */
		global.ve = {
			/**
			 * Registry is initialised from {@link window.ve_registry} if it exists. Otherwise, default settings are used across Vercengen.
			 * @type {Object}
			 */
			registry: (window.ve_registry) ? window.ve_registry :{
				/**
				 * Determines whether or not to run linters at runtime.
				 * @type {boolean}
				 */
				debug_mode: true,
				
				/**
				 * @type {{"<component_key>": ve.Component}}
				 */
				components: {},
				/**
				 * @type {{"<feature_key>": ve.Feature}}
				 */
				features: {},
				/**
				 * Defaults to '' for EN-GB
				 * @type {string}
				 */
				locale: "",
				/**
				 * Localisation strings used inside of Vercengen.
				 * @type {Object}
				 */
				localisation: {},
				/**
				 * Theme keys hold Telestyle CSS objects.
				 * @type {{"<theme_key>": Object}}
				 */
				themes: {},
				
				settings: {
					/**
					 * Whether names can be automatically imputed from the key name. False by default.
					 * @type {boolean}
					 */
					automatic_naming: false,
					
					//Component-wide settings
					
					Blacktraffic: {
						/**
						 * The folder path in which Ontology databases are stored.
						 * @type {string}
						 */
						ontology_saves_folder: "settings/Blacktraffic_ontology/",
						/**
						 * The folder path where worker statuses are stored.
						 * @type {string}
						 */
						worker_saves_folder: "settings/Blacktraffic_workers/"
					},
					
					Channel: {
						/**
						 * The default background colour for consoles.
						 * @type {string}
						 */
						default_bg_colour: "#2196f3",
						/**
						 * The default text colour for consoles. Either 'auto' or on actual colour.
						 * @type {string}
						 */
						default_text_colour: "auto"
					},
					
					Log: {
						/**
						 * Determines the default console height.
						 * @type {string}
						 */
						default_console_height: "40vh"
					},
					
					/**
					 * Component settings for {@link ve.MultiTag}.
					 * @type {{"<registry_key>": string[]}}
					 */
					MultiTag: {
						global: []
					},
					
					NodeEditor: {
						/**
						 * Any window that is currently open to define a script type.
						 * @type {ve.Window|undefined}
						 */
						script_window: undefined,
						/**
						 * Either false if no automatic save file is declared, or the file path to save settings in.
						 * @type {boolean|string}
						 */
						save_file: "settings/NodeEditor_settings.json"
					},
					
					/**
					 * Component settings for {@link ve.ScriptManager}.
					 */
					ScriptManager: {
						/**
						 * Either false if no automatic save file is declared, or the file path to save settings in.
						 * @type {boolean|string}
						 */
						save_file: "settings/ScriptManager_settings.json",
						/**
						 * Determines whether `._settings` are shared between instances of {@link ve.ScriptManager}.
						 * @type {boolean}
						 */
						share_settings_across_instances: true
					},
					
					/**
					 * Component settings for {@link ve.UndoRedo}.
					 */
					UndoRedo: {
						/**
						 * Either false if no automatic save file is declared, or the file path to save settings in.
						 * @type {boolean|string}
						 */
						save_file: "settings/UndoRedo_settings.json",
						
						/**
						 * Whether manual commits are toggled on by default.
						 * @type {boolean}
						 */
						manual_commits: false,
						/**
						 * The default name for manual commits.
						 * @type {string} 
						 */
						manual_commit_name: ""
					}
				}
			}
		};
	
	/**
	 * Clears all Vercengen components from both state and DOM.
	 */
	ve.clear = function () {
		//Iterate over all ve classes and try to close them
		Object.iterate(ve, (local_key, local_value) => {
			if (typeof local_value === "function")
				if (local_value.instances) {
					for (let i = 0; i < local_value.instances.length; i++) {
						if (local_value.instances[i]?.close) try {
							local_value.instances[i].close();
						} catch (e) {}
						if (local_value.instances[i]?.remove) try {
							local_value.instances[i].remove();
						} catch (e) {}
					}
					
					local_value.instances = [];
				}
		});
	};
	
	/**
	 * Returns all non-evaluated files in a folder, so long as an evaluated set is provided.
	 *
	 * @param {string} arg0_folder_path
	 * @param {Set<string>} arg1_evaluated_set
	 *
	 * @returns {Array<string>}
	 */
	ve.getFilesInFolder = function (arg0_folder_path, arg1_evaluated_set) {
		//Convert from parameters
		let folder_path = arg0_folder_path;
		let evaluated_set = arg1_evaluated_set;
		
		//Declare local instance variables
		let file_list = fs.readdirSync(folder_path, { withFileTypes: true });
		let return_files = [];
		
		//Iterate over all entries in file_list
		for (let local_file_entry of file_list) {
			let full_path = path.join(folder_path, local_file_entry.name);
			
			//Skip any full_path in the evaluated_set
			if (evaluated_set.has(full_path)) continue;
				evaluated_set.add(full_path);
				
			//Recursively import files from directories
			if (local_file_entry.isDirectory()) {
				return_files = return_files.concat(ve.getFilesInFolder(full_path, evaluated_set));
			} else {
				return_files.push(full_path);
			}
		}
		
		//Return statement
		return return_files;
	};
	
	/**
	 * Returns an Array<String> from a list of patterns. The last pattern
	 * to match a file determines its final position in the load order. This
	 * function iterates patterns in reverse to ensure that more specific
	 * patterns listed later correctly claim files from broader patterns
	 * listed earlier.
	 *
	 * @param {Array<string>} patterns The list of patterns to resolve.
	 * @returns {Array<string>} The final, ordered list of file paths.
	 */
	ve.getImportFiles = function (patterns) { //[WIP] - Refactor from AI
		let base = process.cwd();
		let finalFiles = [];
		let handledPaths = new Set(); // Tracks files that have already been placed.
		const excludedPaths = new Set(); // Tracks files explicitly excluded by `!`.
		
		// Process patterns in reverse order (from last to first).
		// This is the key to ensuring the "last match wins" rule.
		for (let i = patterns.length - 1; i >= 0; i--) {
			let pattern = patterns[i];
			const isExclusion = pattern.startsWith("!");
			if (isExclusion) {
				pattern = pattern.slice(1);
			}
			
			let files = [];
			if (pattern.includes("*")) {
				files = ve.getWildcardsInFolder(base, pattern);
			} else {
				let absolutePath = path.resolve(base, pattern);
				if (fs.existsSync(absolutePath)) {
					// Use your original, RECURSIVE function for directories.
					if (fs.statSync(absolutePath).isDirectory()) {
						files = ve.getFilesInFolder(absolutePath, new Set());
					} else {
						files = [absolutePath]; // It's a single file.
					}
				}
				// Silently ignore patterns that don't exist.
			}
			
			// Since we're iterating backwards, we prepend files to maintain order.
			// We also process the files found by a pattern in reverse to counteract
			// the unshift, keeping the original readdir order.
			for (let j = files.length - 1; j >= 0; j--) {
				const file = files[j];
				if (isExclusion) {
					excludedPaths.add(file);
				} else {
					// A file is only added if it hasn't been claimed by a later,
					// higher-priority pattern already.
					if (!handledPaths.has(file)) {
						finalFiles.unshift(file);
						handledPaths.add(file);
					}
				}
			}
		}
		
		// Final pass: filter out any files that were explicitly excluded.
		return finalFiles.filter(file => !excludedPaths.has(file));
	};
	
	/**
	 * Returns the absolute file paths of all wildcards within a given folder.
	 *
	 * @param {string} arg0_folder_path
	 * @param {string} arg1_wildcard_pattern
	 *
	 * @returns {Array<string>}
	 */
	ve.getWildcardsInFolder = function (arg0_folder_path, arg1_wildcard_pattern) {
		//Convert from parameters
		let folder_path = arg0_folder_path;
		let wildcard_pattern = arg1_wildcard_pattern;
		
		//Declare local instance variables
		let base = path.basename(wildcard_pattern);
		let directory = path.dirname(wildcard_pattern);
		
		//Non-wildcard resolution
		if (!base.includes("*")) {
			//No wildcard, simply return if the file exists
			let absolute_path = path.resolve(folder_path, wildcard_pattern);
			
			//Return statement
			if (fs.existsSync(absolute_path) && fs.statSync(absolute_path).isFile())
				return [absolute_path];
			if (fs.existsSync(absolute_path) && fs.statSync(absolute_path).isDirectory())
				return ve.getFilesInFolder(absolute_path, new Set());
			return [];
		}
		
		//Wildcard: match files in the directory
		let absolute_dir = path.resolve(folder_path, directory);
		
		//Return statement
		if (!fs.existsSync(absolute_dir) || !fs.statSync(absolute_dir).isDirectory())
			return [];
		
		//Return regex wildcard; return statement
		let regex = new RegExp("^" +
			base.replace(/\./g, "\\.")
				.replace(/\*/g, ".*") + "$"
		);
		
		//Return statement
		return fs.readdirSync(absolute_dir)
			.filter((f) => regex.test(f))
			.map((f) => path.join(absolute_dir, f))
			.filter((f) => fs.statSync(f).isFile());
	};
	
	/**
	 * Initialises Vercengen and associated UF files.
	 */
	ve.initialise = function () {
		//Initialise UF handlers
		new DALS.Timeline(); //Initialise starting timeline
		HTML.initialise();
		
		ve.scene_el = document.createElement("div");
		ve.scene_el.id = "ve-scene";
		ve.scene_el.setAttribute("class", "ve scene");
		document.body.appendChild(ve.scene_el);
		
		ve.window_overlay_el = document.createElement("div");
		ve.window_overlay_el.id = "ve-overlay";
		ve.window_overlay_el.setAttribute("class", "ve overlay");
		document.body.appendChild(ve.window_overlay_el);
		setTimeout(() => {
			ve.Component.linter(); //Lint ve.Component library
			ve.Feature.linter(); //Lint ve.Feature library
		}, 100);
	};
	
	/**
	 * Initialises a Vercengen app, alongside necessary UF imports.
	 *
	 * arg0_options: {@link Object}
	 * - `.do_not_import_UF`: {@link boolean} - Whether to refuse UF imports unrelated to Vercengen startup functions.
	 * - `.load_files`: {@link Array}<{@link string}> - The sequence of files to load. `!` should be used as an exclusion prefix, whilst `*` functions as a wildcard pattern.
	 * - `.is_browser=true`: {@link boolean} - Whether the imports are for the Browser/Electron. Imports are assumed to be eval/Node.js otherwise.
	 * - `.is_node=false`: {@link boolean} - Whether the imports are for Node.js. Overridden by `.is_browser`. Inputs are assumed to be for eval if false.
	 * - `.ontology_function`: {@link function}(arg0_ontologies:{@link Array}<{@link Ontology}>) - The function to execute once Ontology instances are loaded from DBs.
	 * - `.special_function`: {@link function} - The function to execute upon startup and Vercengen initialisation.
	 *
	 * @returns Array<string>
	 */
	ve.start = function (arg0_options) { //[WIP] - Move Browser/Node/Eval selection to `.mode` optioning.
		//Convert from parameters
		let options = (arg0_options) ? arg0_options : {};
		
		//Initialise options
		if (options.is_browser === undefined) options.is_browser = true;
		if (options.load_files === undefined) options.load_files = [];
		
		//Declare local instance variables
		let load_patterns = (!options.do_not_import_UF) ? [
			"!UF/archives",
			"UF",
			
			//Blockly/Maptalks
			"UF/libraries",
			"UF/libraries/blockly.js",
			"UF/libraries/bi_blockly/",
			"!UF/libraries/js2blocks.mjs",
			"UF/libraries/maptalks.min.js",
			"UF/libraries/mapbox-gl.js",
			"UF/libraries/mapbox-gl.css",
			"UF/libraries/maptalks.mapboxgl.min.js",
			"UF/libraries/maptalks.formats.js",
			
			//Leaflet
			"UF/libraries/leaflet.css",
			"UF/libraries/leaflet.js",
			"UF/libraries/leaflet_kmz.js",
			
			//Univer
			"UF/libraries/univer/react.production.min.js",
			"UF/libraries/univer/react-dom.production.min.js",
			"UF/libraries/univer/rxjs.umd.min.js",
			"UF/libraries/univer/univerjs.index.js",
			"UF/libraries/univer/univerjs.umd.index.js",
			"UF/libraries/univer/univer.en-US.js",
			
			//DALS, Vercengen Components
			"UF/js/dals/Timeline.js",
			"UF/js/dals/Timeline_state.js",
			"UF/js/vercengen/engine",
			"UF/js/vercengen/engine/Demo.js",
			"UF/js/vercengen/components",
			//ve.DatavisSuite
			"UF/js/vercengen/components/ComponentDatavisSuite/ComponentDatavisSuite.js",
			"UF/js/vercengen/components/ComponentDatavisSuite/framework",
			"UF/js/vercengen/components/ComponentDatavisSuite/ui",
			//ve.FileExplorer
			"UF/js/vercengen/components/ComponentFileExplorer/file_operations_ui.js",
			//ve.NodeEditor
			"UF/js/vercengen/components/ComponentNodeEditor/ComponentNodeEditor.js",
			"UF/js/vercengen/components/ComponentNodeEditor/",
			//ve.ScriptManager
			"!UF/libraries/monaco/",
			"UF/js/vercengen/components/ComponentScriptManager/blockly/blockly_toolbox.js",
			"UF/js/vercengen/components/ComponentScriptManager/core",
			"UF/js/vercengen/components/ComponentScriptManager/monaco/monaco_startup.js",
			
			"UF/js/vercengen/features",
			
			//Localisation
			"UF/js/vercengen/vercengen_localisation.js",
		] : [];
			load_patterns = load_patterns.concat(options.load_files);
		
		let load_files = ve.getImportFiles(load_patterns);
		
		console.log(`[VERCENGEN] Importing ${load_files.length} files.`);
		
		//1. Handle browser <link>/<script> tags
		
		if (options.is_browser) { //[WIP] - Refactor at a later date
			// Build up the full HTML snippet for all files in order
			let html_concat = "";
			
			for (let i = 0; i < load_files.length; i++) {
				const local_file_path = load_files[i];
				const local_file_extension = path.extname(local_file_path).toLowerCase();
				
				// Each file becomes HTML markup in correct order
				if (local_file_extension === ".css") {
					html_concat += `<link rel="stylesheet" type="text/css" href="${local_file_path}">`;
				} else if (local_file_extension === ".js") {
					// close the script tag safely
					html_concat += `<script type="text/javascript" src="${local_file_path}"></` + `script>`;
				}
			}
			
			// Inject all tags via HTML concatenation
			//
			// document.write() integrates the tags into the parsing process.
			// This ensures that <script> blocks execute *in exact given order*,
			// per HTML parser rules.
			//
			// This must run during document loading (not after DOM is complete).
			injectConcatenatedHTML(html_concat);
		}
		
		//2. Handle eval/Node.js require tags
		if (!options.is_browser) {
			for (let i = 0; i < load_files.length; i++) {
				let local_file_extension = path.extname(load_files[i]);
				let local_file_path = load_files[i];
				
				if (local_file_extension === ".js")
					if (options.is_node) {
						const local_library = require(local_file_path);
						
						//Destructure Node.js objects into global
						for (let [key, value] of Object.entries(local_library)) {
							if (global[key]) {
								console.error(`${key} is already a defined function namespace; ${local_file_path} is attempting an override. The present function is as follows:`, global[key]);
								continue;
							}
							global[key] = value;
						}
					} else {
						eval(fs.readFileSync(local_file_path, "utf8"));
					}
			}
		}
		
		//Initialise ve after import
		global.initialise_ve_loop = setInterval(function(){
			try {
				ve.initialiseThemes();
				ve.initialise();
				
				//Load ontologies
				Ontology.fromDatabase().then(() => {
					if (options.ontology_function)
						options.ontology_function(Ontology.instances);
				});
				global.ve_gc_loop = setInterval(() => {
					//Perform GC
					if (ve?.Tooltip?.refresh) ve.Tooltip.refresh();
				}, 1000);
				
				clearInterval(global.initialise_ve_loop);
				
				if (options.special_function)
					options.special_function();
			} catch (e) {}
		}, 100);
		
		//Return statement
		return load_files;
	};
}

//[WIP] - Refactor later
function injectConcatenatedHTML (htmlMarkup) {
	const tempContainer = document.createElement("div");
	tempContainer.innerHTML = htmlMarkup;
	
	const head = document.head || document.getElementsByTagName("head")[0];
	const body = document.body || document.getElementsByTagName("body")[0];
	
	const fragment = document.createDocumentFragment();
	
	[...tempContainer.children].forEach((el) => {
		if (el.tagName === "SCRIPT") {
			const script = document.createElement("script");
			script.src = el.getAttribute("src");
			script.type = el.type || "text/javascript";
			script.async = false; // preserves order!
			fragment.appendChild(script);
		} else if (el.tagName === "LINK") {
			fragment.appendChild(el);
		}
	});
	
	body.appendChild(fragment);
}