@holochain/hc-spin
Version:
CLI to run Holochain aps during development.
181 lines (157 loc) • 6.14 kB
text/typescript
import path from 'path';
import fs from 'fs';
import url from 'url';
import { AppAuthenticationToken, InstalledAppId } from '@holochain/client';
import { BrowserWindow, NativeImage, nativeImage, net, session, shell } from 'electron';
import { is } from '@electron-toolkit/utils';
import { HappOrWebhappPath } from './validateArgs';
export type UISource =
| {
type: 'path';
path: string;
}
| {
type: 'port';
port: number;
};
export const createHappWindow = async (
uiSource: UISource,
happOrWebhappPath: HappOrWebhappPath,
appId: InstalledAppId,
agentNum: number,
appPort: number,
appAuthToken: AppAuthenticationToken,
appDataRootDir: string,
openDevtools: boolean,
): Promise<BrowserWindow> => {
// TODO create mapping between installed-app-id's and window ids
if (!appPort) throw new Error('App port not defined.');
const partition = `persist:${agentNum}:${appId}`;
if (uiSource.type === 'path') {
const ses = session.fromPartition(partition);
ses.protocol.handle('webhapp', async (request) => {
const uriWithoutProtocol = request.url.slice('webhapp://'.length);
const filePathComponents = uriWithoutProtocol.split('/').slice(1);
const filePath = path.join(...filePathComponents);
return net.fetch(url.pathToFileURL(path.join(uiSource.path, filePath)).toString());
});
}
// Extend preload script to add window.__HC_LAUNCHER_ENV__
let preloadScript = fs.readFileSync(path.join(__dirname, '../preload/index.js')).toString();
preloadScript += `
electron.contextBridge.exposeInMainWorld("__HC_LAUNCHER_ENV__", {
APP_INTERFACE_PORT: ${appPort},
INSTALLED_APP_ID: "${appId}",
APP_INTERFACE_TOKEN: [${appAuthToken}],
});
`;
const preloadPath = path.join(appDataRootDir, `preload-${agentNum}-${appId}.js`);
fs.writeFileSync(preloadPath, preloadScript);
let icon: NativeImage | undefined;
if (uiSource.type === 'path') {
const iconPath = path.join(uiSource.path, 'icon.png');
if (!fs.existsSync(iconPath) && agentNum === 1) {
console.warn(
'\n\n+++++ WARNING +++++\n[hc-spin] No icon.png found. It is recommended to put an icon.png file (1024x1024 pixel) in the root of your UI assets directory which can be used by the Holochain Launcher.\n+++++++++++++++++++\n\n',
);
}
icon = nativeImage.createFromPath(iconPath);
} else {
try {
const iconResponse = await net.fetch(`http://localhost:${uiSource.port}/icon.png`);
if (iconResponse.status === 404 && agentNum === 1) {
console.warn(
'\n\n+++++ WARNING +++++\n[hc-spin] No icon.png found. It is recommended to put an icon.png file (1024x1024 pixel) in the root of your UI assets directory which can be used by the Holochain Launcher.\n+++++++++++++++++++\n\n',
);
}
const buffer = await iconResponse.arrayBuffer();
icon = nativeImage.createFromBuffer(Buffer.from(buffer));
} catch (e) {
console.error('Failed to get icon.png: ', e);
}
}
const happWindow = new BrowserWindow({
width: 1200,
height: 800,
show: false,
icon,
title: `Agent ${agentNum} - ${appId}`,
webPreferences: {
preload: preloadPath,
partition,
},
});
const [windowPositionX, windowPositionY] = happWindow.getPosition();
const windowPositionXMoved = windowPositionX + agentNum * 20;
const windowPositionYMoved = windowPositionY + agentNum * 20;
happWindow.setPosition(windowPositionXMoved, windowPositionYMoved);
happWindow.menuBarVisible = false;
setLinkOpenHandlers(happWindow);
happWindow.on('page-title-updated', (evt) => {
evt.preventDefault();
});
if (openDevtools) happWindow.webContents.openDevTools();
if (uiSource.type === 'port') {
try {
// Check whether dev server is responsive and index.html exists
await net.fetch(`http://localhost:${uiSource.port}/index.html`);
} catch (e) {
console.error(`No index.html file found at http://localhost:${uiSource.port}/index.html`, e);
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
happWindow.loadURL(process.env['ELECTRON_RENDERER_URL']);
} else {
happWindow.loadFile(path.join(__dirname, '../renderer/index.html'));
}
happWindow.show();
return happWindow;
}
await happWindow.loadURL(`http://localhost:${uiSource.port}`);
} else if (uiSource.type === 'path') {
try {
await happWindow.loadURL(`webhapp://webhappwindow/index.html`);
} catch (e) {
console.error('[ERROR] Failed to fetch index.html');
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
happWindow.loadURL(process.env['ELECTRON_RENDERER_URL']);
} else {
const notFoundPath =
happOrWebhappPath.type === 'webhapp'
? path.join(__dirname, '../renderer/indexNotFound1.html')
: path.join(__dirname, '../renderer/indexNotFound2.html');
happWindow.loadFile(notFoundPath);
}
happWindow.show();
return happWindow;
}
} else {
throw new Error('Unsupported uiSource type: ', (uiSource as any).type);
}
happWindow.show();
return happWindow;
};
export function setLinkOpenHandlers(browserWindow: BrowserWindow): void {
// links in happ windows should open in the system default application
// instead of the webview
browserWindow.webContents.on('will-navigate', (e) => {
if (e.url.startsWith('http://localhost') || e.url.startsWith('http://127.0.0.1')) {
// ignore dev server reload
return;
}
if (
e.url.startsWith('http://') ||
e.url.startsWith('https://') ||
e.url.startsWith('mailto://')
) {
e.preventDefault();
shell.openExternal(e.url);
}
});
// Links with target=_blank should open in the system default browser and
// happ windows are not allowed to spawn new electron windows
browserWindow.webContents.setWindowOpenHandler((details) => {
if (details.url.startsWith('http://') || details.url.startsWith('https://')) {
shell.openExternal(details.url);
}
return { action: 'deny' };
});
}