UNPKG

@xuda.io/runtime-bundle

Version:

The Xuda Runtime Bundle refers to a collection of scripts and libraries packaged together to provide the necessary runtime environment for executing plugins or components in the Xuda platform.

1,262 lines (1,127 loc) 45.7 kB
const _this = {}; const normalize_runtime_flag = function (value) { return value === true || value === 'true' || value === 1 || value === '1' ? true : undefined; }; // boaz export const init_module = (e) => { _this.func = e.func; _this.glb = e.glb; _this.SESSION_OBJ = e.SESSION_OBJ; _this.APP_OBJ = e.APP_OBJ; _this.IS_DOCKER = normalize_runtime_flag(e.IS_DOCKER); _this.IS_API_SERVER = normalize_runtime_flag(e.IS_API_SERVER); _this.IS_PROCESS_SERVER = normalize_runtime_flag(e.IS_PROCESS_SERVER); }; const clone_json = function (data) { if (typeof data === 'undefined') { return data; } return JSON.parse(JSON.stringify(data)); }; const get_doc_ts = function (doc) { return Math.max(Number(doc?.ts) || 0, Number(doc?.order_ts) || 0, Number(doc?.docDate) || 0, Number(doc?.studio_meta?.created) || 0, Number(doc?.studio_meta?.migratedTs) || 0); }; const resolve_studio_app_id = function (SESSION_ID, app_id) { const _session = _this.SESSION_OBJ[SESSION_ID]; const resolved_app_id = (app_id && app_id !== 'unknown' && app_id) || (_session?.app_id && _session.app_id !== 'unknown' && _session.app_id) || _session?.url_params?.app_id || _session?.url_params?.id || _session?.build_info?.src_app_id || _session?.build_info?.app_id || (typeof XUDA_BUILD_SRC !== 'undefined' && XUDA_BUILD_SRC) || (typeof XUDA_BUILD_INFO !== 'undefined' && (XUDA_BUILD_INFO?.src_app_id || XUDA_BUILD_INFO?.app_id)) || _this.func?.runtime?.ui?.get_attr?.(_session?.root_element, 'app_id') || null; if (resolved_app_id && resolved_app_id !== 'unknown' && _session && (!_session.app_id || _session.app_id === 'unknown')) { _session.app_id = resolved_app_id; } return resolved_app_id && resolved_app_id !== 'unknown' ? resolved_app_id : null; }; const resolve_draft_runtime_flag = function (SESSION_ID, app_id, ...candidates) { const _session = _this.SESSION_OBJ?.[SESSION_ID]; const candidate_app_ids = [ resolve_studio_app_id(SESSION_ID, app_id), ...candidates.map((candidate) => candidate?._id || candidate?.app_id).filter(Boolean), _session?.app_id, ].filter(Boolean); for (const candidate_app_id of candidate_app_ids) { const app_obj = _this.APP_OBJ?.[candidate_app_id]; if (typeof app_obj?.draft === 'boolean') { return app_obj.draft; } if (typeof app_obj?.is_draft_runtime === 'boolean') { return app_obj.is_draft_runtime; } } for (const candidate of candidates) { if (typeof candidate?.draft === 'boolean') { return candidate.draft; } if (typeof candidate?.is_draft_runtime === 'boolean') { return candidate.is_draft_runtime; } } if (typeof _session?.build_info?.draft === 'boolean') { return _session.build_info.draft; } return false; }; const should_try_local_studio_snapshot = function (SESSION_ID, app_id) { const _session = _this.SESSION_OBJ[SESSION_ID]; const is_server_runtime = !!(_this.IS_DOCKER || _this.IS_PROCESS_SERVER || _this.IS_API_SERVER); const resolved_app_id = resolve_studio_app_id(SESSION_ID, app_id); const is_supported_mode = !_session?.engine_mode || ['miniapp', 'live_preview'].includes(_session.engine_mode); return !is_server_runtime && is_supported_mode && !!resolved_app_id; }; const connect_studio_pouchdb = function (app_id, rt = false, custom) { if (_this.func?.utils?.connect_studio_pouchdb) { return _this.func.utils.connect_studio_pouchdb(app_id, rt, custom); } if (custom) { return new PouchDB(custom, { auto_compaction: true }); } var db_name = 'xuda_studio_db'; if (app_id) { db_name += '_' + app_id; } if (rt) { db_name = `xuda_rt_${app_id}`; } return new PouchDB(db_name, { auto_compaction: true }); }; const connect_studio_resources_pouchdb = function () { return connect_studio_pouchdb(null, false, 'xuda_studio_resources'); }; const upsert_cache_doc = async function (db, doc) { try { const existing_doc = await db.get(doc._id); doc._rev = existing_doc._rev; } catch (error) {} return await db.put(doc); }; const write_cached_build_info = async function (SESSION_ID, build_info_ret) { const db = await func.utils.connect_pouchdb(SESSION_ID); await upsert_cache_doc(db, { _id: `cache_build_info`, build_info: build_info_ret, docType: 'cache_build_info', }); }; const write_cached_rt_info = async function (SESSION_ID, rt_info_ret) { const db = await func.utils.connect_pouchdb(SESSION_ID); await upsert_cache_doc(db, { _id: `cache_rt_info`, data: rt_info_ret, docType: 'cache_app', }); }; const build_draft_error_message = function (app_id, remote_error) { let message = `Unable to load remote app ${app_id}. No local Studio draft was found in browser storage.`; if (remote_error) { message += ` Remote error: ${remote_error}`; } return message; }; const load_active_studio_docs = async function (studio_db) { try { const ret = await studio_db.find({ selector: { docType: 'studio', stat: 3, }, limit: 999999999, }); return ret.docs || []; } catch (error) { const ret = await studio_db.allDocs({ include_docs: true, }); return (ret.rows || []).map((row) => row.doc).filter((doc) => doc?.docType === 'studio' && doc?.stat === 3); } }; const infer_draft_plugin_manifest_entry = function (plugin_name, plugin_doc, dist_path, source_path, extra = {}) { const has_file = function (file_path) { return !!plugin_doc?.files?.[`${plugin_name}/${file_path}`]; }; if (has_file(dist_path)) { return { exist: true, dist: true, is_empty: false, ...extra, }; } if (has_file(source_path)) { return { exist: true, dist: false, is_empty: false, ...extra, }; } return null; }; const normalize_draft_plugins = async function (app_plugins_purchased = {}) { const ret = {}; let resources_db = null; try { resources_db = connect_studio_resources_pouchdb(); } catch (error) {} for (const [plugin_name, plugin_meta] of Object.entries(app_plugins_purchased || {})) { const plugin = clone_json(plugin_meta || {}); if (!plugin.installed) { ret[plugin_name] = plugin; continue; } if (!plugin.manifest) { plugin.manifest = {}; } let plugin_doc = null; if (resources_db) { try { plugin_doc = await resources_db.get(plugin_name); } catch (error) {} } if (!plugin.manifest['runtime.mjs']) { plugin.manifest['runtime.mjs'] = infer_draft_plugin_manifest_entry(plugin_name, plugin_doc, 'dist/runtime.mjs', 'runtime.mjs', { css: !!plugin_doc?.files?.[`${plugin_name}/dist/runtime.css`], }) || { exist: true, dist: true, is_empty: false, css: false, }; } if (!plugin.manifest['index.mjs']) { plugin.manifest['index.mjs'] = infer_draft_plugin_manifest_entry(plugin_name, plugin_doc, 'dist/index.mjs', 'index.mjs') || { exist: true, dist: true, is_empty: false, }; } ret[plugin_name] = plugin; } return ret; }; const build_draft_docs_map = function (app_id, studio_docs = []) { const docs_obj = {}; let globals_doc = null; for (const doc of studio_docs) { const cloned_doc = clone_json(doc); docs_obj[cloned_doc._id] = cloned_doc; if (cloned_doc?.properties?.menuType === 'globals') { globals_doc = cloned_doc; } } if (globals_doc && !docs_obj[`global_${app_id}`]) { docs_obj[`global_${app_id}`] = { ...clone_json(globals_doc), _id: `global_${app_id}`, }; } return docs_obj; }; const get_local_draft_snapshot = async function (SESSION_ID, app_id) { app_id = resolve_studio_app_id(SESSION_ID, app_id); if (!should_try_local_studio_snapshot(SESSION_ID, app_id)) { return null; } const studio_db = connect_studio_pouchdb(app_id); let app_doc; try { app_doc = await studio_db.get(app_id); } catch (error) { return null; } const studio_docs = await load_active_studio_docs(studio_db); const docs_obj = build_draft_docs_map(app_id, studio_docs); const draft_docs = Object.values(docs_obj); const tables = []; const programs = []; for (const doc of draft_docs) { const menu_type = doc?.properties?.menuType; if (menu_type === 'table') { tables.push(doc._id); continue; } if (menu_type === 'globals') { continue; } programs.push(doc._id); } let last_changed_ts = get_doc_ts(app_doc); for (const doc of draft_docs) { last_changed_ts = Math.max(last_changed_ts, get_doc_ts(doc)); } const build = last_changed_ts || 0; const app_doc_data = clone_json(app_doc) || {}; const is_draft_runtime = _this.SESSION_OBJ?.[SESSION_ID]?.engine_mode === 'live_preview' ? !!app_doc_data.draft : true; delete app_doc_data._rev; const rt_info_obj = { ...app_doc_data, _id: app_id, app_id, draft: is_draft_runtime, is_deployment: false, is_draft_runtime, app_type: app_doc_data.app_type || 'master', app_build_id: build.toString(), app_id_reference: app_doc_data.app_id_reference || app_id, app_replicate: app_doc_data.app_replicate || app_id, app_db_name: app_doc_data.app_db_name || app_id, app_plugins_purchased: await normalize_draft_plugins(app_doc_data.app_plugins_purchased || {}), account_info: app_doc_data.account_info || {}, login_info: app_doc_data.login_info || {}, deploy_data: clone_json(app_doc_data.deploy_data || {}), client_ip: '', rpi_http_methods: ['get_doc_obj_from_build', 'dbs_read', 'dbs_create', 'dbs_update', 'dbs_delete'], accessible_deployed_progs_arr: programs, accessible_deployed_tables_arr: tables, }; const build_info = { build, src_app_id: app_id, master_build: build, server_ts: last_changed_ts, last_changed_ts, progs_changed: programs, tables_changed: tables, draft: is_draft_runtime, }; return { build_info, build_info_ret: { code: 1, data: build_info, }, rt_info: rt_info_obj, rt_info_ret: { code: 1, data: rt_info_obj, }, docs_obj, }; }; const persist_draft_snapshot = async function (SESSION_ID, draft_snapshot) { await write_cached_build_info(SESSION_ID, draft_snapshot.build_info_ret); await write_cached_rt_info(SESSION_ID, draft_snapshot.rt_info_ret); }; export const project_loader = async function (SESSION_ID, app_id, prog_id) { try { var _session = _this.SESSION_OBJ[SESSION_ID]; app_id = resolve_studio_app_id(SESSION_ID, app_id) || app_id; if (app_id && app_id !== 'unknown') { _session.app_id = app_id; } if (_this.func.UI.utils.get_url_attribute(SESSION_ID, 'clear_cache')) { await func.index.delete_pouch(SESSION_ID); } let last_changed_ts = 0; var ret_build_info = {}; let draft_snapshot = null; if (_session.engine_mode === 'live_preview') { draft_snapshot = await get_local_draft_snapshot(SESSION_ID, app_id); if (draft_snapshot) { ret_build_info = draft_snapshot.build_info_ret; _session.build_info = draft_snapshot.build_info; _session.is_draft_runtime = resolve_draft_runtime_flag(SESSION_ID, app_id, draft_snapshot.rt_info, draft_snapshot.build_info); await persist_draft_snapshot(SESSION_ID, draft_snapshot); last_changed_ts = draft_snapshot.build_info.last_changed_ts || 0; } else { if (!IS_ONLINE) { return _this.func.UI.utils.progressScreen.show(SESSION_ID, build_draft_error_message(app_id), null, true); } ret_build_info = await get_app_build_info(SESSION_ID, app_id); if (ret_build_info.code < 0) { return _this.func.UI.utils.progressScreen.show(SESSION_ID, build_draft_error_message(app_id, ret_build_info.data), null, true); } _session.build_info = ret_build_info.data; _session.is_draft_runtime = resolve_draft_runtime_flag(SESSION_ID, app_id, ret_build_info?.data); await write_cached_build_info(SESSION_ID, ret_build_info); last_changed_ts = ret_build_info.data?.last_changed_ts || ret_build_info.data?.server_ts || 0; } } else if (typeof XUDA_BUILD_INFO !== 'undefined') { // call from xuda real-preview app _session.build_info = XUDA_BUILD_INFO; _session.is_draft_runtime = resolve_draft_runtime_flag(SESSION_ID, app_id, _session.build_info); if (_session.is_draft_runtime) { draft_snapshot = await get_local_draft_snapshot(SESSION_ID, app_id); if (!draft_snapshot) { return _this.func.UI.utils.progressScreen.show(SESSION_ID, build_draft_error_message(app_id), null, true); } ret_build_info = draft_snapshot.build_info_ret; _session.build_info = draft_snapshot.build_info; _session.is_draft_runtime = resolve_draft_runtime_flag(SESSION_ID, app_id, draft_snapshot.rt_info, draft_snapshot.build_info); await persist_draft_snapshot(SESSION_ID, draft_snapshot); } last_changed_ts = _session.build_info?.last_changed_ts || 0; } else { // test const db = await func.utils.connect_pouchdb(SESSION_ID); if (['live_preview', 'miniapp'].includes(_session.engine_mode)) { // if on-line if (IS_ONLINE) { ret_build_info = await get_app_build_info(SESSION_ID, app_id); if (ret_build_info.code < 0) { draft_snapshot = await get_local_draft_snapshot(SESSION_ID, app_id); if (!draft_snapshot) { return _this.func.UI.utils.progressScreen.show(SESSION_ID, build_draft_error_message(app_id, ret_build_info.data), null, true); } ret_build_info = draft_snapshot.build_info_ret; _session.is_draft_runtime = resolve_draft_runtime_flag(SESSION_ID, app_id, draft_snapshot.rt_info, draft_snapshot.build_info); await persist_draft_snapshot(SESSION_ID, draft_snapshot); } else { _session.is_draft_runtime = resolve_draft_runtime_flag(SESSION_ID, app_id, ret_build_info?.data); } _session.build_info = ret_build_info.data; await write_cached_build_info(SESSION_ID, ret_build_info); } else { // off-line get from pouch try { ret_build_info = (await db.get(`cache_build_info`)).build_info; _session.build_info = ret_build_info.data; _session.is_draft_runtime = resolve_draft_runtime_flag(SESSION_ID, app_id, ret_build_info?.data); } catch (error) { draft_snapshot = await get_local_draft_snapshot(SESSION_ID, app_id); if (!draft_snapshot) { return console.error('cache_build_info error'); } ret_build_info = draft_snapshot.build_info_ret; _session.build_info = draft_snapshot.build_info; _session.is_draft_runtime = resolve_draft_runtime_flag(SESSION_ID, app_id, draft_snapshot.rt_info, draft_snapshot.build_info); await persist_draft_snapshot(SESSION_ID, draft_snapshot); } } last_changed_ts = ret_build_info.data.last_changed_ts; } else { // deployments try { // const db = await func.utils.connect_pouchdb(SESSION_ID); // only get indication for fresh installation await db.get(`cache_rt_info`); } catch (err) { // fresh load const startup_module = await func.common.get_module(SESSION_ID, 'xuda-deploy-startup-loader.mjs'); await startup_module.loader(SESSION_ID); } last_changed_ts = _session.opt.last_changed_ts; } } // if (_session.engine_mode !== 'live_preview') { // try { // const db = await func.utils.connect_pouchdb(SESSION_ID); // // only get indication for fresh installation // await db.get(`cache_rt_info`); // } catch (err) { // const startup_module = await func.common.get_module(SESSION_ID, 'xuda-deploy-startup-loader.mjs'); // await startup_module.loader(SESSION_ID); // } // last_changed_ts = _session.opt.last_changed_ts; // } const rt_info_ret = await get_rt_info(SESSION_ID, app_id, last_changed_ts); const active_draft_snapshot = rt_info_ret?.draft_snapshot || draft_snapshot; await insert_custom_prop(SESSION_ID); if (_session?.app_admin_prop?.app_admin_direction) { _session.root_element.setAttribute('dir', _session.app_admin_prop.app_admin_direction); } // load live preview module const progs_loader_module = await _this.func.common.get_module(SESSION_ID, `xuda-progs-loader-module.mjs`); if (_session.is_draft_runtime && active_draft_snapshot?.docs_obj) { await progs_loader_module.prime_objects_cache(SESSION_ID, 'DOCS_OBJ', active_draft_snapshot.docs_obj); } else if (_session.app_id !== 'unknown' && _session.engine_mode !== 'live_preview') { await progs_loader_module.load_objects_cache(SESSION_ID); } if (_session.engine_mode === 'live_preview') { const live_preview_module = await _this.func.common.get_module(SESSION_ID, 'xuda-live-preview-module.esm.js'); live_preview_module.live_preview_loader(SESSION_ID); return; } if (_this.APP_OBJ[_session.app_id]?.is_deployment) { try { await init_runtime_websocket(SESSION_ID, _session.app_id); } catch (error) { throw error; } } await _this.func.UI.main.embed_loader(SESSION_ID); } catch (error) { throw error; } }; const get_app_build_info = async function (SESSION_ID, app_id) { return new Promise(function (resolve, reject) { var _session = _this.SESSION_OBJ[SESSION_ID]; let app_id_reference = _this.APP_OBJ[app_id].app_id_reference; fetch(_this.func.common.get_url(SESSION_ID, 'rpi', 'get_app_build'), { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'xu-gtp-token': _session.gtp_token, 'xu-app-token': _session.app_token, }, body: JSON.stringify({ app_id: app_id, app_id_reference, engine_mode: _session.engine_mode, }), }) .then((response) => { if (!response.ok) { return response.text().then((text) => { throw new Error(text); }); } return response.json(); }) .then(async (json) => { _this.SESSION_OBJ[SESSION_ID].build_info = json.data; resolve(json); }) .catch((err) => { try { resolve(JSON.parse(err.message)); } catch (error) { resolve({ code: -1, data: err.message }); } }); }); }; const get_user_group_account_info = async function (SESSION_ID, uid) { var _session = SESSION_OBJ[SESSION_ID]; const response = await fetch(`https://${_session.domain}/cpi/get_account_info`, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify({ uid, uid_query: document.cookie .split('; ') .find((c) => c.startsWith('uid=')) ?.split('=')[1] || '', }), }); const json = await response.json(); return json.data; }; const get_rt_info = async function (SESSION_ID, app_id, last_changed_ts) { var _session = _this.SESSION_OBJ[SESSION_ID]; app_id = resolve_studio_app_id(SESSION_ID, app_id) || app_id; return new Promise(async function (resolve, reject) { const response = { success: async function (ret, ajaxP) { if (ret.code < 0) { return response.error(ret.data); } const rt_info_obj = ret.data; var app_id = rt_info_obj._id; _this.APP_OBJ[app_id] = rt_info_obj; _session.app_id = app_id; if (rt_info_obj?.deploy_data?.global_variables) { _session.url_params = { ..._session.url_params, ...rt_info_obj.deploy_data.global_variables, }; } let account_info = { ...(rt_info_obj?.account_info || {}) }; if (_session.engine_mode === 'user_group') { const user_group_data = await get_user_group_account_info(SESSION_ID, rt_info_obj?.account_info?.uid); account_info = user_group_data.account_info; account_info.uid = user_group_data._id; } _session.USR_OBJ = { _id: account_info?.uid, usr_name: account_info?.username, usr_first_name: account_info?.first_name || account_info?.email, usr_last_name: account_info?.last_name || '', usr_email: account_info?.email, usr_profile_picture: account_info?.profile_picture, }; _session.login_info = rt_info_obj?.login_info; _session.client_ip = rt_info_obj.client_ip; _session.rpi_http_methods = rt_info_obj.rpi_http_methods; _session.app_admin_prop = rt_info_obj.app_admin_prop; _session.is_deployment = rt_info_obj.is_deployment; _session.is_draft_runtime = resolve_draft_runtime_flag(SESSION_ID, app_id, rt_info_obj); // const set_prog_cache = async function () { // if (_session.prog_id) { // if (!rt_info_obj.prog_docs[_session.prog_id]) { // return console.error( // `error - program ${_session.prog_id} not found.` // ); // } // DOCS_OBJ[app_id][_session.prog_id] = rt_info_obj.prog_docs[ // _session.prog_id // ] || { // _id: _session.prog_id, // }; // const module = await _this.func.common.get_module( // SESSION_ID, // `xuda-progs-loader-module.mjs` // ); // module.save_objects_cache( // SESSION_ID, // _session.prog_id, // "DOCS_OBJ", // rt_info_obj.prog_docs[_session.prog_id] // ); // } // // for (let prog_id of [ // // ...rt_info_obj?.accessible_deployed_progs_arr, // // ...rt_info_obj?.accessible_deployed_tables_arr, // // ] || []) { // // module.DOCS_OBJ_get(SESSION_ID, prog_id); // // } // }; // if (_session.engine_mode !== "live_preview") { // set_prog_cache(); // } resolve({ rt_info_obj, draft_snapshot: ret?.draft_snapshot || null, }); const loaderLogo = document.querySelector('.loader_logo'); if (loaderLogo) { const app_pic = typeof rt_info_obj.app_pic === 'string' ? rt_info_obj.app_pic.trim() : ''; if (app_pic && app_pic !== 'undefined' && app_pic !== 'null') { loaderLogo.style.backgroundImage = `url( ${app_pic})`; } else { loaderLogo.style.removeProperty('background-image'); } loaderLogo.style.display = 'none'; loaderLogo.style.display = ''; } }, error: async function (err) { if (err) { return _this.func.UI.utils.progressScreen.show(SESSION_ID, err, null, true); } // location.reload(); console.warn('** reload request'); }, }; const db = await func.utils.connect_pouchdb(SESSION_ID); if (_session.engine_mode === 'live_preview') { const draft_snapshot = await get_local_draft_snapshot(SESSION_ID, app_id); if (draft_snapshot) { await persist_draft_snapshot(SESSION_ID, draft_snapshot); response.success({ ...draft_snapshot.rt_info_ret, draft_snapshot, }); return; } if (resolve_draft_runtime_flag(SESSION_ID, app_id, _session?.build_info)) { return response.error(build_draft_error_message(app_id)); } } if (resolve_draft_runtime_flag(SESSION_ID, app_id, _session?.build_info)) { const draft_snapshot = await get_local_draft_snapshot(SESSION_ID, app_id); if (!draft_snapshot) { return response.error(build_draft_error_message(app_id)); } await persist_draft_snapshot(SESSION_ID, draft_snapshot); response.success({ ...draft_snapshot.rt_info_ret, draft_snapshot, }); return; } try { let ret = await db.get(`cache_rt_info`); const rt_info_obj = ret.data; if (rt_info_obj.data.last_changed_ts !== last_changed_ts || (!['live_preview', 'miniapp'].includes(_session.engine_mode) && rt_info_obj.data.app_build_id !== _session.opt.app_build_id)) { _this.func.UI.utils.progressScreen.show(SESSION_ID, 'New application setup detected, refreshing data and reloading in 5 sec'); setTimeout(async () => { await func.index.delete_pouch(SESSION_ID); location.reload(); }, 5000); return; } response.success(rt_info_obj); } catch (err) { fetch(_this.func.common.get_url(SESSION_ID, 'rpi', 'get_rt_info'), { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'xu-gtp-token': _session.gtp_token, 'xu-app-token': _session.app_token, }, body: JSON.stringify({ app_id, prog_id: _session.engine_mode === 'live_preview' ? '' : _session.prog_id, engine_mode: _session.engine_mode, build: _session.build_info?.build, session_id: SESSION_ID, is_cordova: _this.glb.is_cordova, client_info: _session.SYS_GLOBAL_OBJ_CLIENT_INFO, client_id: _session.SYS_GLOBAL_OBJ_CLIENT_INFO.fingerprint, }), }) .then((response) => { if (!response.ok) { return response.text().then((text) => { throw new Error(text); }); } return response.json(); }) .then((json) => { if (json.code < 0) { throw new Error(json.data || 'unknown error'); } response.success(json); }) .then(async (json) => { if (json?.code > 0) { await write_cached_rt_info(SESSION_ID, json); } }) .catch(async (err) => { if (should_try_local_studio_snapshot(SESSION_ID, app_id)) { const draft_snapshot = await get_local_draft_snapshot(SESSION_ID, app_id); if (draft_snapshot) { await persist_draft_snapshot(SESSION_ID, draft_snapshot); response.success({ ...draft_snapshot.rt_info_ret, draft_snapshot, }); return; } response.error(build_draft_error_message(app_id, err.message)); return; } response.error(err.message); }); } }); }; // Parse an HTML string and return a fragment whose <script> tags are RECREATED as fresh // elements so the browser EXECUTES them (innerHTML/insertAdjacentHTML-parsed scripts are // flagged non-executable forever, per the HTML spec). src scripts get async=false to // preserve document order (vue.global.js must run before the inline modules that use it, // the tailwind bundle before its inline config, etc.). Returns { frag, loads } where // loads are the src-script load promises (each already error-tolerant) so the caller can // wait for the libs before depending on their globals. const build_executable_fragment = function (html) { const tpl = document.createElement('template'); tpl.innerHTML = html; const loads = []; const recreate_script = function (node) { const s = document.createElement('script'); for (const a of Array.from(node.attributes)) s.setAttribute(a.name, a.value); s.setAttribute('data-xuda-executable-fragment', 'true'); const script_type = `${s.getAttribute('type') || ''}`.trim().toLowerCase(); const transform_inline_module = function (text) { const imports = []; let body = `${text || ''}`; body = body.replace(/^\s*import\s+([A-Za-z_$][\w$]*)\s+from\s+["']([^"']+)["'];?\s*/gm, function (_match, local_name, url) { imports.push(`try { ${local_name} = (await import(${JSON.stringify(url)})).default; } catch (error) { console.error(error); }`); return ''; }); body = body.replace(/^\s*import\s+\{\s*([^}]+?)\s*\}\s+from\s+["']([^"']+)["'];?\s*/gm, function (_match, local_names, url) { imports.push(`try { ({ ${local_names.trim()} } = await import(${JSON.stringify(url)})); } catch (error) { console.error(error); }`); return ''; }); if (!imports.length || /\bexport\s+/.test(body)) { return ''; } const declarations = imports .map((line) => line.match(/^\s*try\s+\{\s*(?:\(\{\s*)?([A-Za-z_$][\w$]*)/)?.[1]) .filter(Boolean) .map((name) => `let ${name};`) .join('\n'); return `;(async function () {\n${declarations}\n${imports.join('\n')}\n${body}\nconst __xuda_schedule_tailwind_refresh = function (reason, delay) { if (!window.__xudaScheduleTailwindRefresh) { const state = window.__xudaTailwindRefreshState ||= { timer: 0, running: false, pending: false }; window.__xudaScheduleTailwindRefresh = function (_reason, requestedDelay) { if (state.running) { state.pending = true; return; } if (state.timer) { state.pending = true; return; } state.timer = setTimeout(async function () { state.timer = 0; state.running = true; state.pending = false; try { window.__xudaTailwindNormalizeTheme?.(); window.__xudaTailwindNormalizePlugins?.(); await window.tailwind?.refresh?.(); } catch (error) { console.error(error); } finally { state.running = false; if (state.pending) { state.pending = false; window.__xudaScheduleTailwindRefresh('pending', 180); } } }, requestedDelay || 80); }; } window.__xudaScheduleTailwindRefresh(reason, delay || 80); }; __xuda_schedule_tailwind_refresh('inline-module', 80); setTimeout(() => __xuda_schedule_tailwind_refresh('inline-module-settle', 180), 600);\n})();`; }; const inline_module_text = !node.src && script_type === 'module' ? transform_inline_module(node.textContent || '') : ''; if (inline_module_text) { s.removeAttribute('type'); s.setAttribute('data-xuda-inline-module', 'classic-dynamic-import'); s.text = inline_module_text; } const should_wait_for_script = !!node.src || (script_type === 'module' && !inline_module_text); let inline_module_blob_url = ''; if ( !inline_module_text && !node.src && script_type === 'module' && typeof URL !== 'undefined' && typeof URL.createObjectURL === 'function' && typeof Blob !== 'undefined' ) { inline_module_blob_url = URL.createObjectURL(new Blob([node.textContent || ''], { type: 'text/javascript' })); s.src = inline_module_blob_url; s.setAttribute('data-xuda-inline-module', 'blob'); } if (should_wait_for_script) { loads.push( new Promise((res) => { const done = function () { if (inline_module_blob_url) { setTimeout(() => URL.revokeObjectURL(inline_module_blob_url), 0); } res(); }; s.onload = s.onerror = done; }), ); } if (node.src) { if (!s.hasAttribute('async')) { s.async = false; } } else if (!inline_module_blob_url && !inline_module_text) { s.text = node.textContent; } return s; }; const clone_executable_node = function (node) { if (node.nodeType === Node.TEXT_NODE || node.nodeType === Node.COMMENT_NODE) { return node.cloneNode(true); } if (node.nodeType !== Node.ELEMENT_NODE) { return node.cloneNode(true); } if (`${node.tagName || ''}`.toLowerCase() === 'script') { return recreate_script(node); } const clone = node.cloneNode(false); for (const child_node of Array.from(node.childNodes)) { clone.appendChild(clone_executable_node(child_node)); } return clone; }; const frag = document.createDocumentFragment(); for (const child_node of Array.from(tpl.content.childNodes)) { frag.appendChild(clone_executable_node(child_node)); } return { frag, loads }; }; const insert_custom_prop = async function (SESSION_ID) { try { var app_id = _this.SESSION_OBJ[SESSION_ID].app_id; // user_group (published group apps, e.g. drive.modulartalmud.com) was built against the // OLD runtime, which executed the app_custom_header/body scripts (Vue global, tailwind // bundle + inline config, uploaded up_*.js, body sortable/draggable). The dead-script // injection below breaks all of those at once (unstyled tailwind buttons, missing Vue). // So for user_group we inject through an EXECUTING fragment; every other engine mode // keeps the legacy inert injection untouched (preview is verified working with it). const exec_mode = _this.SESSION_OBJ[SESSION_ID]?.engine_mode === 'user_group'; let header_loads = []; if (_this.APP_OBJ[app_id]?.app_custom_prop?.app_custom_header) { var head = document.getElementsByTagName('head')[0]; const app_custom_header = _this.APP_OBJ[app_id].app_custom_prop.app_custom_header; const header_html = func.utils.replace_studio_drive_url(SESSION_ID, app_custom_header); if (exec_mode) { const { frag, loads } = build_executable_fragment(header_html); head.appendChild(frag); header_loads = loads; } else { head.insertAdjacentHTML('beforeend', header_html); } } if (_this.APP_OBJ[app_id]?.app_custom_prop?.app_custom_body) { const root_el = _this.SESSION_OBJ[SESSION_ID].root_element; const body_html = _this.APP_OBJ[app_id].app_custom_prop.app_custom_body; if (exec_mode) { const { frag, loads } = build_executable_fragment(body_html); root_el.insertBefore(frag, root_el.firstChild); header_loads = header_loads.concat(loads); } else { root_el.insertAdjacentHTML('afterbegin', body_html); } } // Don't let a stalled CDN block render: cap the wait, same rule as the gallery libs. if (header_loads.length) { await Promise.race([Promise.all(header_loads), new Promise((res) => setTimeout(res, 4000))]); } // Gallery libs (Swiper + FsLightbox) are declared in app_custom_header, but <script> tags // inserted via insertAdjacentHTML are fetched-NOT-executed, so they expose no globals. // Force-load them through the executable loader BEFORE screen_ready (the FsLightbox // controller reads window.fsLightbox / refreshFsLightbox / fsLightboxInstances as bare // globals on screen_ready). TWO safety rules learned the hard way: // 1) append "?xu_lib=1" so the URL does NOT match the dead header <script> — otherwise // load_script attaches a load-listener to that already-settled, never-executed node // whose load event already fired, so the callback never runs and the await hangs. // 2) cap each load with a hard timeout so render is NEVER blocked, even if a lib stalls. const rep = _this.APP_OBJ[app_id]?.app_replicate; if (rep && typeof document !== 'undefined') { const drv = (p) => func.utils.replace_studio_drive_url(SESSION_ID, `https://xuda.ai/studio-drive/${rep}/${p}`) + '?xu_lib=1'; const capped = (pr) => Promise.race([pr, new Promise((res) => setTimeout(res, 4000))]); await Promise.all([ capped(func.utils.load_js_on_demand(drv('node_modules/swiper/swiper-bundle.min.js'), '')), capped(func.utils.load_js_on_demand(drv('fslightbox_new.js'), '')), ]); // Wrap Swiper so every carousel (a) scopes its nav selectors to its OWN container — the app // reuses the same global '.swiper-button-next/prev' for every carousel, so the bare global // selector binds them all to the FIRST button in the document and the visible gallery's // arrows control nothing; and (b) observes DOM changes so slides/nav added after init get // picked up. Idempotent via __xuWrapped; falls back to the original selector if unscopable. const Sw = typeof window !== 'undefined' ? window.Swiper : undefined; if (Sw && !Sw.__xuWrapped) { const Wrapped = function (el, opts) { opts = Object.assign({ observer: true, observeParents: true, observeSlideChildren: true }, opts || {}); const root = typeof el === 'string' ? document.querySelector(el) : el; const nav = opts.navigation; if (nav && root) { // The buttons live OUTSIDE the .swiper element (siblings in the gallery panel, not // children), so a container-only query misses them and falls back to the shared global // selector → wrong button. Resolve each selector to THIS carousel's button by walking UP // from the container to the nearest ancestor that contains it. const scope = (sel) => { if (typeof sel !== 'string') return sel; let n = root; for (let i = 0; i < 5 && n; i++) { const hit = n.querySelector && n.querySelector(sel); if (hit) return hit; n = n.parentElement; } return sel; }; if (nav.nextEl) nav.nextEl = scope(nav.nextEl); if (nav.prevEl) nav.prevEl = scope(nav.prevEl); } return new Sw(el, opts); }; for (const k in Sw) { try { Wrapped[k] = Sw[k]; } catch (e) {} } Wrapped.prototype = Sw.prototype; Wrapped.__xuWrapped = true; window.Swiper = Wrapped; if (typeof globalThis !== 'undefined') globalThis.Swiper = Wrapped; } } } catch (err) { console.error(err); } }; const init_runtime_websocket = function (SESSION_ID, app_id) { return new Promise(function (resolve, reject) { const set_connected = async function (stat) { var datasource_changes = { [0]: { ['data_system']: { SYS_GLOBAL_BOL_CONNECTED: stat }, }, }; await func.datasource.update(SESSION_ID, datasource_changes); }; //////////////// IO ////////////////// var _session = _this.SESSION_OBJ[SESSION_ID]; var _data_system = _session?.DS_GLB?.[0]?.data_system; const url = `https://${_this.func.runtime.platform.get_hostname()}`; var error; RUNTIME_SERVER_WEBSOCKET = io(url, { secure: true, reconnection: true, // _this.glb.debug_js ? false : true, // reconnection: true, // reconnectionDelayMax: 10000, rejectUnauthorized: false, path: '/ws/socket.io', // transports: ['websocket'] // query: { session_obj: SESSION_OBJ[SESSION_ID] }, }); RUNTIME_SERVER_WEBSOCKET.on('connect', () => { console.info('RUNTIME_SERVER_WEBSOCKET connected'); if (_data_system) { set_connected(1); } if (_session.opt.enable_offline) { if (_session.root_element.classList.contains('runtime_offline')) { _session.root_element.classList.remove('runtime_offline'); func.utils.alerts.toast(SESSION_ID, 'Switched to on-line mode', 'You are now online. All data stored while you were offline will be synchronized to the server.', 'success'); } } else { // back from temporarily disconnected from the server _this.func.UI.utils.progressScreen.hide(SESSION_ID); } if (error) { if (!RUNTIME_SERVER_WEBSOCKET_CONNECTED) { // location.reload(); console.warn('** reload request'); } } }); RUNTIME_SERVER_WEBSOCKET.on('message', (e) => { if (_this.APP_OBJ[app_id].is_deployment) { _this.func.UI.utils.indicator.server.busy(); setTimeout( function () { _this.func.UI.utils.indicator.server.normal(); }, e?.data?.length * 100 || 100, ); } var data = e.data; if (e.source === 'http_call') { if (e.service === 'get_doc_obj_from_build') { return func.runtime.platform.emit('get_doc_obj_from_build_response_' + data._id, { data, }); } if (e.service === 'heartbeat') { return func.runtime.platform.emit('heartbeat_response', { data, }); } return func.runtime.platform.emit('get_ws_data_response_' + e.websocket_queue_num, { data, e, }); } if (e.source === 'deployment_server') { console.log('document_changed', e); return func.runtime.ui.refresh_document_changes_for_realtime_update(SESSION_ID, e.data); } if (data !== 'connected') return; RUNTIME_SERVER_WEBSOCKET_CONNECTED = true; resolve(); }); var callback_done = false; RUNTIME_SERVER_WEBSOCKET.on('connect_error', (error) => { if (!callback_done) { resolve(); callback_done = true; error = true; } }); RUNTIME_SERVER_WEBSOCKET.on('disconnect', async () => { RUNTIME_SERVER_WEBSOCKET_CONNECTED = false; if (_data_system) { set_connected(0); } if (_session.opt.enable_offline) { _session.root_element.classList.add('runtime_offline'); func.utils.alerts.toast(SESSION_ID, 'Switched to off-line mode', 'You have lost connection to the server and are now working offline. Once the connection is restored, all data will be synchronized.', 'warning'); } else { // await func.index.delete_pouch(); // window.location.href = `https://${_session.domain}/error?error_code=408`; _this.func.UI.utils.progressScreen.show(SESSION_ID, 'Your browser has temporarily disconnected from the server. Please wait while we attempt to reconnect.'); } }); _this.func.runtime.platform.add_window_listener('beforeunload', function (event) { var obj = { service: 'close_websocket', data: { session_id: SESSION_ID }, }; RUNTIME_SERVER_WEBSOCKET.emit('message', obj); }); }); }; export const run_plugins_runtime_init = async function (SESSION_ID, app_id, method) { var _session = _this.SESSION_OBJ[SESSION_ID]; const get_plugin_method_resource = function (manifest, method) { const direct = manifest?.[`${method}.mjs`]; if (direct?.exist && !direct.is_empty) { return `${direct.dist ? 'dist/' : ''}${method}.mjs`; } const legacy = manifest?.[method]?.mjs; if (legacy?.exist && !legacy.is_empty) { return `${legacy.dist ? 'dist/' : ''}${method}.mjs`; } return null; }; const append_runtime_import_cache_tag = function (url) { const cache_tag = (typeof globalThis !== 'undefined' ? globalThis.__XU_RUNTIME_MODULE_CACHE_TAG__ || globalThis.__XU_SERVER_BOOTSTRAP__?.version : '') || _session?.build_info?.runtime_ts || _session?.build_info?.last_changed_ts || _session?.build_info?.server_ts || _session?.opt?.app_build_id || 0; if (!cache_tag) { return url; } return `${url}${url.includes('?') ? '&' : '?'}xu_runtime_ts=${encodeURIComponent(cache_tag)}`; }; const get_path = function (plugin_name, resource) { if (_session.worker_type === 'Dev') { return append_runtime_import_cache_tag(`../../plugins/${_session.domain}/${plugin_name}/${resource}`); } return append_runtime_import_cache_tag(`https://${_session.domain}/plugins/${plugin_name}/runtime/${resource}?gtp_token=${_session.gtp_token}&app_id=${_session.app_id}`); }; const load_plugin_runtime_module = async function (plugin_name, resource) { if (typeof _this.func?.utils?.get_plugin_resource === 'function') { return await _this.func.utils.get_plugin_resource(SESSION_ID, plugin_name, resource); } const local_plugin_resource_url = await _this.func?.utils?.get_local_studio_plugin_resource_url?.(SESSION_ID, plugin_name, resource); if (local_plugin_resource_url) { return await import(/* @vite-ignore */ local_plugin_resource_url); } return await import(get_path(plugin_name, resource)); }; if (!_this.APP_OBJ[app_id].app_plugins_purchased) return; for await (const [key, val] of Object.entries(_this.APP_OBJ[app_id].app_plugins_purchased)) { if (!val.installed) continue; const plugin_resource = get_plugin_method_resource(val.manifest, method); if (!plugin_resource) continue; let module; try { module = await load_plugin_runtime_module(key, plugin_resource); } catch (error) { console.error(error); await _this.func?.utils?.report_issue?.(SESSION_ID, { code: 'RUN_MSG_GUI_020', source: 'run_plugins_runtime_init', message: `plugin runtime import failed for ${key}`, err: error, details: { plugin_name: key, plugin_resource, method, }, }); continue; } try { await module.default({ SESSION_ID: SESSION_ID }); } catch (error) { console.error(error); } } };