Merge pull request #422 from HeyPuter/eric/gui-loading

GUI loading refactor + service-registered gui scripts
This commit is contained in:
Eric Dubé
2024-05-24 17:42:14 -04:00
committed by GitHub
7 changed files with 311 additions and 255 deletions
+3
View File
@@ -213,6 +213,9 @@ const install = async ({ services, app }) => {
const { LockService } = require('./services/LockService');
services.registerService('lock', LockService);
const { PuterHomepageService } = require('./services/PuterHomepageService');
services.registerService('puter-homepage', PuterHomepageService);
}
const install_legacy = async ({ services }) => {
+19 -2
View File
@@ -79,6 +79,7 @@ config.puter_hosted_data = {
const path_ = require('path');
config.assets = {
gui: path_.join(__dirname, '../../..'),
gui_profile: 'development',
};
}
@@ -174,9 +175,25 @@ const config_pointer = {};
// We'd like to store values changed at runtime separately
// for easier runtime debugging
{
const config_runtime_values = {};
const config_runtime_values = {
$: 'runtime-values'
};
config_runtime_values.__proto__ = config_to_export;
config_to_export = config_runtime_values
// These can be difficult to find and cause painful
// confusing issues, so we log any time this happens
config_to_export = new Proxy(config_to_export, {
set: (target, prop, value, receiver) => {
console.log(
'\x1B[36;1mCONFIGURATION MUTATED AT RUNTIME\x1B[0m',
prop, 'to', value
);
// console.log(new Error('stack trace to find configuration mutation'));
target[prop] = value;
return true;
}
})
}
module.exports = config_to_export;
module.exports = config_to_export;
+8 -42
View File
@@ -23,7 +23,6 @@ const router = express.Router();
const _path = require('path');
const _fs = require('fs');
const auth = require('../middleware/auth.js');
const { generate_puter_page_html } = require('../temp/puter_page_loader');
const { Context } = require('../util/context');
const { DB_READ } = require('../services/database/consts');
const { PathBuilder } = require('../util/pathutil.js');
@@ -302,47 +301,14 @@ router.all('*', async function(req, res, next) {
// index.js
if(path === '/'){
const APP_ORIGIN = config.origin;
const API_ORIGIN = config.api_base_url;
return res.send(generate_puter_page_html({
env: config.env,
app_origin: APP_ORIGIN,
api_origin: API_ORIGIN,
use_bundled_gui: config.use_bundled_gui,
manifest,
gui_path: config.assets.gui,
// page meta
meta: {
title: app_title,
description: description || config.short_description,
short_description: config.short_description,
company: 'Puter Technologies Inc.',
canonical_url: canonical_url,
},
// gui parameters
gui_params: {
app_name_regex: config.app_name_regex,
app_name_max_length: config.app_name_max_length,
app_title_max_length: config.app_title_max_length,
subdomain_regex: config.subdomain_regex,
subdomain_max_length: config.subdomain_max_length,
domain: config.domain,
protocol: config.protocol,
env: config.env,
api_base_url: config.api_base_url,
thumb_width: config.thumb_width,
thumb_height: config.thumb_height,
contact_email: config.contact_email,
max_fsentry_name_length: config.max_fsentry_name_length,
require_email_verification_to_publish_website: config.require_email_verification_to_publish_website,
short_description: config.short_description,
long_description: config.long_description,
},
}));
const svc_puterHomepage = Context.get('services').get('puter-homepage');
return svc_puterHomepage.send(res, {
title: app_title,
description: description || config.short_description,
short_description: config.short_description,
company: 'Puter Technologies Inc.',
canonical_url: canonical_url,
});
}
// /dist/...
@@ -0,0 +1,241 @@
const { PathBuilder } = require("../util/pathutil");
const BaseService = require("./BaseService");
/**
* PuterHomepageService serves the initial HTML page that loads the Puter GUI
* and all of its assets.
*/
class PuterHomepageService extends BaseService {
static MODULES = {
fs: require('node:fs'),
}
_construct () {
this.service_scripts = [];
}
async _init () {
// Load manifest
const config = this.global_config;
const manifest_raw = this.modules.fs.readFileSync(
PathBuilder
.add(config.assets.gui, { allow_traversal: true })
.add('puter-gui.json')
.build(),
'utf8'
);
const manifest_data = JSON.parse(manifest_raw);
this.manifest = manifest_data[config.assets.gui_profile];
}
register_script (url) {
this.service_scripts.push(url);
}
async send (res, meta) {
const config = this.global_config;
return res.send(this.generate_puter_page_html({
env: config.env,
app_origin: config.origin,
api_origin: config.api_base_url,
use_bundled_gui: config.use_bundled_gui,
manifest: this.manifest,
gui_path: config.assets.gui,
// page meta
meta,
// gui parameters
gui_params: {
app_name_regex: config.app_name_regex,
app_name_max_length: config.app_name_max_length,
app_title_max_length: config.app_title_max_length,
subdomain_regex: config.subdomain_regex,
subdomain_max_length: config.subdomain_max_length,
domain: config.domain,
protocol: config.protocol,
env: config.env,
api_base_url: config.api_base_url,
thumb_width: config.thumb_width,
thumb_height: config.thumb_height,
contact_email: config.contact_email,
max_fsentry_name_length: config.max_fsentry_name_length,
require_email_verification_to_publish_website: config.require_email_verification_to_publish_website,
short_description: config.short_description,
long_description: config.long_description,
},
}));
}
generate_puter_page_html ({
env,
manifest,
gui_path,
use_bundled_gui,
app_origin,
api_origin,
meta,
gui_params,
}) {
const require = this.require;
const {encode} = require('html-entities');
const path_ = require('path');
const fs_ = require('fs');
const e = encode;
const {
title,
description,
short_description,
company,
canonical_url,
} = meta;
gui_params = {
...meta,
...gui_params,
app_origin,
api_origin,
gui_origin: app_origin,
};
const asset_dir = env === 'dev'
? '/src' : '/dist' ;
// const asset_dir = '/dist';
gui_params.asset_dir = asset_dir;
const bundled = env != 'dev' || use_bundled_gui;
return `<!DOCTYPE html>
<html lang="en">
<head>
<title>${e(title)}</title>
<meta name="author" content="${e(company)}">
<meta name="description" content="${e((description).replace(/\n/g, " "))}">
<meta name="facebook-domain-verification" content="e29w3hjbnnnypf4kzk2cewcdaxym1y" />
<link rel="canonical" href="${e(canonical_url)}">
<!-- Meta meta tags -->
<meta property="og:url" content="${app_origin}">
<meta property="og:type" content="website">
<meta property="og:title" content="${e(title)}">
<meta property="og:description" content="${e((short_description).replace(/\n/g, " "))}">
<meta property="og:image" content="${asset_dir}/images/screenshot.png">
<!-- Twitter meta tags -->
<meta name="twitter:card" content="summary_large_image">
<meta property="twitter:domain" content="puter.com">
<meta property="twitter:url" content="${app_origin}">
<meta name="twitter:title" content="${e(title)}">
<meta name="twitter:description" content="${e((short_description).replace(/\n/g, " "))}">
<meta name="twitter:image" content="${asset_dir}/images/screenshot.png">
<!-- favicons -->
<link rel="apple-touch-icon" sizes="57x57" href="${asset_dir}/favicons/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="${asset_dir}/favicons/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="${asset_dir}/favicons/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="${asset_dir}/favicons/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="${asset_dir}/favicons/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="${asset_dir}/favicons/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="${asset_dir}/favicons/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="${asset_dir}/favicons/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="${asset_dir}/favicons/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="${asset_dir}/favicons/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="${asset_dir}/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="${asset_dir}/favicons/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="${asset_dir}/favicons/favicon-16x16.png">
<link rel="manifest" href="${asset_dir}/manifest.json">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="${asset_dir}/favicons/ms-icon-144x144.png">
<meta name="theme-color" content="#ffffff">
<!-- Preload images when applicable -->
<link rel="preload" as="image" href="${asset_dir}/images/wallpaper.webp">
<!-- Files from JSON (may be empty) -->
${
((!bundled && manifest?.css_paths)
? manifest.css_paths.map(path => `<link rel="stylesheet" href="${path}">\n`)
: []).join('')
}
<!-- END Files from JSON -->
</head>
<body>
<script>window.puter_gui_enabled = true;</script>
${
use_bundled_gui
? `<script>window.gui_env = 'prod';</script>`
: ''
}
${
((!bundled && manifest?.lib_paths)
? manifest.lib_paths.map(path => `<script type="text/javascript" src="${path}"></script>\n`)
: []).join('')
}
<script>
window.icons = {};
${(() => {
if ( !(!bundled && manifest) ) return '';
const html = [];
fs_.readdirSync(path_.join(gui_path, 'src/icons')).forEach(file => {
// skip dotfiles
if(file.startsWith('.'))
return;
// load image
let buff = new Buffer.from(fs_.readFileSync(path_.join(gui_path, 'src/icons') + '/' + file));
// convert to base64
let base64data = buff.toString('base64');
// add to `window.icons`
if(file.endsWith('.png'))
html.push(`window.icons['${file}'] = "data:image/png;base64,${base64data}";\n`);
else if(file.endsWith('.svg'))
html.push(`window.icons['${file}'] = "data:image/svg+xml;base64,${base64data}";\n`);
})
return html.join('');
})()}
</script>
${
((!bundled && manifest?.js_paths)
? manifest.js_paths.map(path => `<script type="module" src="${path}"></script>\n`)
: []).join('')
}
${
this.service_scripts
.map(path => `<script type="module" src="${path}"></script>\n`)
.join('')
}
<!-- Load the GUI script -->
<script ${ !bundled ? ' type="module"' : ''} src="${(!bundled && manifest?.index) || '/dist/gui.js'}"></script>
<!-- Initialize GUI when document is loaded -->
<script>
window.addEventListener('load', function() {
gui(${
// TODO: override JSON.stringify to ALWAYS to this...
// this should be an opt-OUT, not an opt-IN!
JSON.stringify(gui_params).replace(/</g, '\\u003c')
});
});
</script>
<div id="templates" style="display: none;"></div>
</body>
</html>`;
};
}
module.exports = {
PuterHomepageService
};
@@ -0,0 +1,5 @@
/**
* ServeLandingService is for "landing" pages, like payment success or failure.
*/
class ServceLandingService {}
@@ -1,181 +0,0 @@
/*
* Copyright (C) 2024 Puter Technologies Inc.
*
* This file is part of Puter.
*
* Puter is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const {encode} = require('html-entities');
const path_ = require('path');
const fs_ = require('fs');
const generate_puter_page_html = ({
env,
manifest,
gui_path,
use_bundled_gui,
app_origin,
api_origin,
meta,
gui_params,
}) => {
const e = encode;
const {
title,
description,
short_description,
company,
canonical_url,
} = meta;
gui_params = {
...meta,
...gui_params,
app_origin,
api_origin,
gui_origin: app_origin,
};
const asset_dir = env === 'dev'
? '/src' : '/dist' ;
// const asset_dir = '/dist';
gui_params.asset_dir = asset_dir;
const bundled = env != 'dev' || use_bundled_gui;
return `<!DOCTYPE html>
<html lang="en">
<head>
<title>${e(title)}</title>
<meta name="author" content="${e(company)}">
<meta name="description" content="${e((description).replace(/\n/g, " "))}">
<meta name="facebook-domain-verification" content="e29w3hjbnnnypf4kzk2cewcdaxym1y" />
<link rel="canonical" href="${e(canonical_url)}">
<!-- Meta meta tags -->
<meta property="og:url" content="${app_origin}">
<meta property="og:type" content="website">
<meta property="og:title" content="${e(title)}">
<meta property="og:description" content="${e((short_description).replace(/\n/g, " "))}">
<meta property="og:image" content="${asset_dir}/images/screenshot.png">
<!-- Twitter meta tags -->
<meta name="twitter:card" content="summary_large_image">
<meta property="twitter:domain" content="puter.com">
<meta property="twitter:url" content="${app_origin}">
<meta name="twitter:title" content="${e(title)}">
<meta name="twitter:description" content="${e((short_description).replace(/\n/g, " "))}">
<meta name="twitter:image" content="${asset_dir}/images/screenshot.png">
<!-- favicons -->
<link rel="apple-touch-icon" sizes="57x57" href="${asset_dir}/favicons/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="${asset_dir}/favicons/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="${asset_dir}/favicons/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="${asset_dir}/favicons/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="${asset_dir}/favicons/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="${asset_dir}/favicons/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="${asset_dir}/favicons/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="${asset_dir}/favicons/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="${asset_dir}/favicons/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="${asset_dir}/favicons/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="${asset_dir}/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="${asset_dir}/favicons/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="${asset_dir}/favicons/favicon-16x16.png">
<link rel="manifest" href="${asset_dir}/manifest.json">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="${asset_dir}/favicons/ms-icon-144x144.png">
<meta name="theme-color" content="#ffffff">
<!-- Preload images when applicable -->
<link rel="preload" as="image" href="${asset_dir}/images/wallpaper.webp">
<!-- Files from JSON (may be empty) -->
${
((!bundled && manifest?.css_paths)
? manifest.css_paths.map(path => `<link rel="stylesheet" href="${path}">\n`)
: []).join('')
}
<!-- END Files from JSON -->
</head>
<body>
<script>window.puter_gui_enabled = true;</script>
${
use_bundled_gui
? `<script>window.gui_env = 'prod';</script>`
: ''
}
${
((!bundled && manifest?.lib_paths)
? manifest.lib_paths.map(path => `<script type="text/javascript" src="${path}"></script>\n`)
: []).join('')
}
<script>
window.icons = {};
${(() => {
if ( !(!bundled && manifest) ) return '';
const html = [];
fs_.readdirSync(path_.join(gui_path, 'src/icons')).forEach(file => {
// skip dotfiles
if(file.startsWith('.'))
return;
// load image
let buff = new Buffer.from(fs_.readFileSync(path_.join(gui_path, 'src/icons') + '/' + file));
// convert to base64
let base64data = buff.toString('base64');
// add to `window.icons`
if(file.endsWith('.png'))
html.push(`window.icons['${file}'] = "data:image/png;base64,${base64data}";\n`);
else if(file.endsWith('.svg'))
html.push(`window.icons['${file}'] = "data:image/svg+xml;base64,${base64data}";\n`);
})
return html.join('');
})()}
</script>
${
((!bundled && manifest?.js_paths)
? manifest.js_paths.map(path => `<script type="module" src="${path}"></script>\n`)
: []).join('')
}
<!-- Load the GUI script -->
<script ${ !bundled ? ' type="module"' : ''} src="${(!bundled && manifest?.index) || '/dist/gui.js'}"></script>
<!-- Initialize GUI when document is loaded -->
<script>
window.addEventListener('load', function() {
gui(${
// TODO: override JSON.stringify to ALWAYS to this...
// this should be an opt-OUT, not an opt-IN!
JSON.stringify(gui_params).replace(/</g, '\\u003c')
});
});
</script>
<div id="templates" style="display: none;"></div>
</body>
</html>`;
};
module.exports = {
generate_puter_page_html,
};
+35 -30
View File
@@ -1,32 +1,37 @@
{
"index": "/src/index.js",
"lib_paths": [
"/lib/jquery-3.6.1/jquery-3.6.1.min.js",
"/lib/viselect.min.js",
"/lib/FileSaver.min.js",
"/lib/socket.io/socket.io.min.js",
"/lib/qrcode.min.js",
"/lib/jquery-ui-1.13.2/jquery-ui.min.js",
"/lib/lodash@4.17.21.min.js",
"/lib/jquery.dragster.js",
"/lib/jquery.menu-aim.js",
"/lib/html-entities.js",
"/lib/timeago.min.js",
"/lib/iro.min.js",
"/lib/isMobile.min.js",
"/lib/jszip-3.10.1.min.js"
],
"css_paths": [
"/css/normalize.css",
"/lib/jquery-ui-1.13.2/jquery-ui.min.css",
"/css/style.css",
"/css/theme.css"
],
"js_paths": [
"/src/initgui.js",
"/src/helpers.js",
"/src/IPC.js",
"/src/globals.js",
"/src/i18n/i18n.js"
]
"development": {
"index": "/src/index.js",
"lib_paths": [
"/lib/jquery-3.6.1/jquery-3.6.1.min.js",
"/lib/viselect.min.js",
"/lib/FileSaver.min.js",
"/lib/socket.io/socket.io.min.js",
"/lib/qrcode.min.js",
"/lib/jquery-ui-1.13.2/jquery-ui.min.js",
"/lib/lodash@4.17.21.min.js",
"/lib/jquery.dragster.js",
"/lib/jquery.menu-aim.js",
"/lib/html-entities.js",
"/lib/timeago.min.js",
"/lib/iro.min.js",
"/lib/isMobile.min.js",
"/lib/jszip-3.10.1.min.js"
],
"css_paths": [
"/css/normalize.css",
"/lib/jquery-ui-1.13.2/jquery-ui.min.css",
"/css/style.css",
"/css/theme.css"
],
"js_paths": [
"/src/initgui.js",
"/src/helpers.js",
"/src/IPC.js",
"/src/globals.js",
"/src/i18n/i18n.js"
]
},
"bundle": {
"index": ["/dist/gui.js", false]
}
}