mirror of
https://github.com/HeyPuter/puter.git
synced 2026-05-04 00:20:45 +00:00
feat: support extension divs headers and tags being inserted to puter homepage load (#2221)
Docker Image CI / build-and-push-image (push) Has been cancelled
Maintain Release Merge PR / update-release-pr (push) Has been cancelled
release-please / release-please (push) Has been cancelled
test / test-backend (24.x) (push) Has been cancelled
test / API tests (node env, api-test) (24.x) (push) Has been cancelled
test / puterjs (node env, vitest) (24.x) (push) Has been cancelled
Docker Image CI / build-and-push-image (push) Has been cancelled
Maintain Release Merge PR / update-release-pr (push) Has been cancelled
release-please / release-please (push) Has been cancelled
test / test-backend (24.x) (push) Has been cancelled
test / API tests (node env, api-test) (24.x) (push) Has been cancelled
test / puterjs (node env, vitest) (24.x) (push) Has been cancelled
* feat: support extension divs headers and tags being inserted to puter homepage load * wip: demo div * fix: extension typing * fix: extension typing * feat: hompage gui add on
This commit is contained in:
Vendored
+97
-39
@@ -10,32 +10,36 @@ import type { SUService } from '@heyputer/backend/src/services/SUService.js';
|
||||
import type { IUser } from '@heyputer/backend/src/services/User.js';
|
||||
import type { UserService } from '@heyputer/backend/src/services/UserService.d.ts';
|
||||
import { Context } from '@heyputer/backend/src/util/context.js';
|
||||
import kvjs from '@heyputer/kv.js';
|
||||
import type { RequestHandler } from 'express';
|
||||
import type FSNodeContext from '../src/backend/src/filesystem/FSNodeContext.js';
|
||||
import type helpers from '../src/backend/src/helpers.js';
|
||||
import type * as ExtensionControllerExports from './ExtensionController/src/ExtensionController.ts';
|
||||
import kvjs from '@heyputer/kv.js';
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
services: { get: <T extends (keyof ServiceNameMap) | (string & {})>(string: T) => T extends keyof ServiceNameMap ? ServiceNameMap[T] : unknown }
|
||||
actor: Actor,
|
||||
rawBody: Buffer,
|
||||
services: {
|
||||
get: <T extends keyof ServiceNameMap | (string & {})>(
|
||||
string: T,
|
||||
) => T extends keyof ServiceNameMap ? ServiceNameMap[T] : unknown;
|
||||
};
|
||||
actor: Actor;
|
||||
rawBody: Buffer;
|
||||
/** @deprecated use actor instead */
|
||||
user: IUser
|
||||
user: IUser;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface EndpointOptions {
|
||||
allowedMethods?: string[]
|
||||
subdomain?: string
|
||||
noauth?: boolean
|
||||
mw?: RequestHandler[]
|
||||
allowedMethods?: string[];
|
||||
subdomain?: string;
|
||||
noauth?: boolean;
|
||||
mw?: RequestHandler[];
|
||||
otherOpts?: Record<string, unknown> & {
|
||||
json?: boolean
|
||||
noReallyItsJson?: boolean
|
||||
}
|
||||
json?: boolean;
|
||||
noReallyItsJson?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// Driver interface types
|
||||
@@ -54,7 +58,11 @@ interface DriverInterface {
|
||||
|
||||
type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch';
|
||||
|
||||
export type AddRouteFunction = (path: string, options: EndpointOptions, handler: RequestHandler) => void;
|
||||
export type AddRouteFunction = (
|
||||
path: string,
|
||||
options: EndpointOptions,
|
||||
handler: RequestHandler,
|
||||
) => void;
|
||||
|
||||
export type RouterMethods = {
|
||||
[K in HttpMethod]: {
|
||||
@@ -65,44 +73,94 @@ export type RouterMethods = {
|
||||
|
||||
interface CoreRuntimeModule {
|
||||
util: {
|
||||
helpers: typeof helpers,
|
||||
}
|
||||
Context: typeof Context,
|
||||
APIError: typeof APIError
|
||||
helpers: typeof helpers;
|
||||
};
|
||||
Context: typeof Context;
|
||||
APIError: typeof APIError;
|
||||
}
|
||||
|
||||
interface FilesystemModule {
|
||||
FSNodeContext: FSNodeContext,
|
||||
selectors: unknown,
|
||||
FSNodeContext: FSNodeContext;
|
||||
selectors: unknown;
|
||||
}
|
||||
|
||||
type StripPrefix<TPrefix extends string, T extends string> = T extends `${TPrefix}.${infer R}` ? R : never;
|
||||
type StripPrefix<
|
||||
TPrefix extends string,
|
||||
T extends string,
|
||||
> = T extends `${TPrefix}.${infer R}` ? R : never;
|
||||
// TODO DS: define this globally in core to use it there too
|
||||
interface ServiceNameMap {
|
||||
'meteringService': Pick<MeteringServiceWrapper, 'meteringService'> & MeteringService // TODO DS: squash into a single class without wrapper
|
||||
'puter-kvstore': DynamoKVStore
|
||||
'su': SUService
|
||||
'database': BaseDatabaseAccessService
|
||||
'user': UserService
|
||||
'web-server': WebServerService
|
||||
meteringService: Pick<MeteringServiceWrapper, 'meteringService'> &
|
||||
MeteringService; // TODO DS: squash into a single class without wrapper
|
||||
'puter-kvstore': DynamoKVStore;
|
||||
su: SUService;
|
||||
database: BaseDatabaseAccessService;
|
||||
user: UserService;
|
||||
'web-server': WebServerService;
|
||||
}
|
||||
|
||||
export interface ExtensionEventTypeMap {
|
||||
'create.drivers': {
|
||||
createDriver: (interface: string, service: string, executors: any) => any;
|
||||
};
|
||||
'create.permissions': {
|
||||
grant_to_everyone: (permission: string) => void;
|
||||
grant_to_users: (permission: string) => void;
|
||||
};
|
||||
'create.interfaces': {
|
||||
createInterface: (interface: string, interfaces: DriverInterface) => void;
|
||||
};
|
||||
'puter.gui.addons': {
|
||||
bodyContent: string;
|
||||
headContent: string;
|
||||
guiParams: {
|
||||
env: string;
|
||||
app_origin: string;
|
||||
api_origin: string;
|
||||
gui_origin: string;
|
||||
asset_dir: string;
|
||||
launch_options: unknown;
|
||||
app_name_regex: RegExp;
|
||||
app_name_max_length: number;
|
||||
app_title_max_length: number;
|
||||
hosting_domain: string;
|
||||
subdomain_regex: RegExp;
|
||||
subdomain_max_length: number;
|
||||
domain: string;
|
||||
protocol: string;
|
||||
api_base_url: string;
|
||||
app?: unknown;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface Extension extends RouterMethods {
|
||||
exports: Record<string, unknown>,
|
||||
exports: Record<string, unknown>;
|
||||
span: (<T>(label: string, fn: () => T) => () => T) & {
|
||||
run<T>(label: string, fn: () => T): T;
|
||||
run<T>(fn: () => T): T;
|
||||
},
|
||||
config: Record<string | number | symbol, any>,
|
||||
on<T extends unknown[]>(event: string, listener: (...args: T) => void): void, // TODO DS: type events better
|
||||
on(event: 'create.drivers', listener: (event: { createDriver: (interface: string, service: string, executors: any) => any }) => void),
|
||||
on(event: 'create.permissions', listener: (event: { grant_to_everyone: (permission: string) => void, grant_to_users: (permission: string) => void }) => void)
|
||||
on(event: 'create.interfaces', listener: (event: { createInterface: (interface: string, interfaces: DriverInterface) => void }) => void)
|
||||
import(module: 'data'): { db: BaseDatabaseAccessService, kv: DynamoKVStore, cache: kvjs }
|
||||
import(module: 'core'): CoreRuntimeModule,
|
||||
import(module: 'fs'): FilesystemModule,
|
||||
import(module: 'query'): typeof query,
|
||||
import(module: 'extensionController'): typeof ExtensionControllerExports
|
||||
import<T extends `service:${keyof ServiceNameMap}` | (string & {})>(module: T): T extends `service:${infer R extends keyof ServiceNameMap}`
|
||||
};
|
||||
config: Record<string | number | symbol, any>;
|
||||
|
||||
on<E extends keyof ExtensionEventTypeMap>(
|
||||
name: E,
|
||||
listener: (event: ExtensionEventTypeMap[E]) => void | Promise<void>
|
||||
): void;
|
||||
on(name: string, listener: (event: unknown) => void | Promise<void>): void
|
||||
|
||||
import(module: 'data'): {
|
||||
db: BaseDatabaseAccessService;
|
||||
kv: DynamoKVStore;
|
||||
cache: kvjs;
|
||||
};
|
||||
import(module: 'core'): CoreRuntimeModule;
|
||||
import(module: 'fs'): FilesystemModule;
|
||||
import(module: 'query'): typeof query;
|
||||
import(module: 'extensionController'): typeof ExtensionControllerExports;
|
||||
import<T extends `service:${keyof ServiceNameMap}` | (string & {})>(
|
||||
module: T
|
||||
): T extends `service:${infer R extends keyof ServiceNameMap}`
|
||||
? ServiceNameMap[R]
|
||||
: unknown;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
extension.on('puter.gui.addons', async (event) => {
|
||||
if ( event.guiParams.app ) {
|
||||
// disabled for now
|
||||
// const app = event.guiParams.app;
|
||||
// event.bodyContent += `
|
||||
// <div style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 9999999999; background: rgba(0,0,0,0.8); color: white; padding: 20px; overflow: auto;">
|
||||
// test: ${ JSON.stringify(app)}
|
||||
// </div>`;
|
||||
// event.headContent += `<meta name="description" content="some additional description"/>`
|
||||
// event.headContent += `<script> console.log("test1234"); </script>`
|
||||
}
|
||||
});
|
||||
@@ -329,6 +329,7 @@ router.all('*', async function (req, res, next) {
|
||||
// GUI
|
||||
// ------------------------
|
||||
else {
|
||||
let app;
|
||||
let canonical_url = config.origin + path;
|
||||
let app_name, app_title, app_description, app_icon, app_social_media_image;
|
||||
let launch_options = {
|
||||
@@ -353,7 +354,7 @@ router.all('*', async function (req, res, next) {
|
||||
// /app/
|
||||
else if ( path.startsWith('/app/') ) {
|
||||
app_name = path.replace('/app/', '');
|
||||
const app = await get_app({
|
||||
app = await get_app({
|
||||
follow_old_names: true,
|
||||
name: app_name,
|
||||
});
|
||||
@@ -388,14 +389,6 @@ router.all('*', async function (req, res, next) {
|
||||
path = '/';
|
||||
}
|
||||
|
||||
const manifest =
|
||||
_fs.existsSync(_path.join(config.assets.gui, 'puter-gui.json'))
|
||||
? (() => {
|
||||
const text = _fs.readFileSync(_path.join(config.assets.gui, 'puter-gui.json'), 'utf8');
|
||||
return JSON.parse(text);
|
||||
})()
|
||||
: {};
|
||||
|
||||
// index.js
|
||||
if ( path === '/' ) {
|
||||
const svc_puterHomepage = Context.get('services').get('puter-homepage');
|
||||
@@ -407,6 +400,7 @@ router.all('*', async function (req, res, next) {
|
||||
company: 'Puter Technologies Inc.',
|
||||
canonical_url: canonical_url,
|
||||
icon: app_icon,
|
||||
app: app,
|
||||
}, launch_options);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,20 +16,17 @@
|
||||
* 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 { PathBuilder } = require('../util/pathutil');
|
||||
const BaseService = require('./BaseService');
|
||||
const { is_valid_url } = require('../helpers');
|
||||
const { Endpoint } = require('../util/expressutil');
|
||||
const { Context } = require('../util/context');
|
||||
|
||||
import { encode } from 'html-entities';
|
||||
import { is_valid_url } from '../helpers.js';
|
||||
import { Endpoint } from '../util/expressutil.js';
|
||||
import { PathBuilder } from '../util/pathutil.js';
|
||||
import BaseService from './BaseService.js';
|
||||
import fs from 'node:fs';
|
||||
/**
|
||||
* 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'),
|
||||
};
|
||||
export class PuterHomepageService extends BaseService {
|
||||
|
||||
_construct () {
|
||||
this.service_scripts = [];
|
||||
@@ -45,7 +42,7 @@ class PuterHomepageService extends BaseService {
|
||||
async _init () {
|
||||
// Load manifest
|
||||
const config = this.global_config;
|
||||
const manifest_raw = this.modules.fs.readFileSync(PathBuilder
|
||||
const manifest_raw = fs.readFileSync(PathBuilder
|
||||
.add(config.assets.gui, { allow_traversal: true })
|
||||
.add('puter-gui.json')
|
||||
.build(),
|
||||
@@ -122,7 +119,7 @@ class PuterHomepageService extends BaseService {
|
||||
// cloudflare turnstile site key
|
||||
const turnstileSiteKey = config.services?.['cloudflare-turnstile']?.enabled ? config.services?.['cloudflare-turnstile']?.site_key : null;
|
||||
|
||||
return res.send(this.generate_puter_page_html({
|
||||
return res.send(await this.generate_puter_page_html({
|
||||
env: config.env,
|
||||
|
||||
app_origin: config.origin,
|
||||
@@ -144,7 +141,7 @@ class PuterHomepageService extends BaseService {
|
||||
app_name_max_length: config.app_name_max_length,
|
||||
app_title_max_length: config.app_title_max_length,
|
||||
hosting_domain: config.static_hosting_domain +
|
||||
(config.pub_port !== 80 && config.pub_port !== 443 ? `:${ config.pub_port}` : ''),
|
||||
(config.pub_port !== 80 && config.pub_port !== 443 ? `:${config.pub_port}` : ''),
|
||||
subdomain_regex: config.subdomain_regex,
|
||||
subdomain_max_length: config.subdomain_max_length,
|
||||
domain: config.domain,
|
||||
@@ -167,23 +164,19 @@ class PuterHomepageService extends BaseService {
|
||||
}));
|
||||
}
|
||||
|
||||
generate_puter_page_html ({
|
||||
async generate_puter_page_html ({
|
||||
env,
|
||||
|
||||
manifest,
|
||||
gui_path,
|
||||
gui_path: _gui_path,
|
||||
use_bundled_gui,
|
||||
|
||||
app_origin,
|
||||
api_origin,
|
||||
|
||||
meta,
|
||||
launch_options,
|
||||
|
||||
gui_params,
|
||||
}) {
|
||||
const require = this.require;
|
||||
const { encode } = require('html-entities');
|
||||
|
||||
const eventService = this.services.get('event');
|
||||
|
||||
const e = encode;
|
||||
|
||||
@@ -207,7 +200,7 @@ class PuterHomepageService extends BaseService {
|
||||
};
|
||||
|
||||
const asset_dir = env === 'dev'
|
||||
? '/src' : '/dist' ;
|
||||
? '/src' : '/dist';
|
||||
|
||||
gui_params.asset_dir = asset_dir;
|
||||
|
||||
@@ -239,11 +232,21 @@ class PuterHomepageService extends BaseService {
|
||||
custom_script_tags_str += tag;
|
||||
}
|
||||
|
||||
// emit extension event
|
||||
const event = {
|
||||
bodyContent: '',
|
||||
headContent: '',
|
||||
guiParams: {
|
||||
...gui_params,
|
||||
},
|
||||
};
|
||||
await eventService.emit('puter.gui.addons', event);
|
||||
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, ' ').trim())}">
|
||||
<meta name="facebook-domain-verification" content="e29w3hjbnnnypf4kzk2cewcdaxym1y" />
|
||||
@@ -323,21 +326,27 @@ class PuterHomepageService extends BaseService {
|
||||
</script>
|
||||
|
||||
<!-- Files from JSON (may be empty) -->
|
||||
${
|
||||
((!bundled && manifest?.css_paths)
|
||||
${((!bundled && manifest?.css_paths)
|
||||
? manifest.css_paths.map(path => `<link rel="stylesheet" href="${path}">\n`)
|
||||
: []).join('')
|
||||
}
|
||||
<!-- END Files from JSON -->
|
||||
|
||||
<!-- Custom header content to be added tthe homepage by extensions -->
|
||||
${event.headContent || ''}
|
||||
<!-- END Custom header -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- Custom body content to be added to the homepage by extensions -->
|
||||
${event.bodyContent || ''}
|
||||
<!-- END Custom body content -->
|
||||
|
||||
<script>window.puter_gui_enabled = true;</script>
|
||||
${
|
||||
custom_script_tags_str
|
||||
${custom_script_tags_str
|
||||
}
|
||||
${
|
||||
use_bundled_gui
|
||||
${use_bundled_gui
|
||||
? '<script>window.gui_env = \'prod\';</script>'
|
||||
: ''
|
||||
}
|
||||
@@ -369,19 +378,18 @@ class PuterHomepageService extends BaseService {
|
||||
});
|
||||
</script>
|
||||
<!-- Initialize Service Scripts -->
|
||||
${
|
||||
this.service_scripts
|
||||
${this.service_scripts
|
||||
.map(path => `<script type="module" src="${path}"></script>\n`)
|
||||
.join('')
|
||||
}
|
||||
<div id="templates" style="display: none;"></div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>`;
|
||||
};
|
||||
|
||||
generate_error_html ({ message }) {
|
||||
const { encode } = require('html-entities');
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
@@ -407,15 +415,10 @@ class PuterHomepageService extends BaseService {
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>${
|
||||
encode(message, { mode: 'nonAsciiPrintable' })
|
||||
<h1>${encode(message, { mode: 'nonAsciiPrintable' })
|
||||
}</h1>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
PuterHomepageService,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user