Skip to content
Permalink

Comparing changes

This is a direct comparison between two commits made in this repository or its related repositories. View the default comparison for this range or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: phcode-dev/phcode.live
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: ad2e8338046278ef355e1add5dc82c292f299066
Choose a base ref
..
head repository: phcode-dev/phcode.live
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: f726e4de51218d40ae60740efd64d8651b5bcf64
Choose a head ref
1 change: 1 addition & 0 deletions CNAME
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
phcode.live
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,36 @@
# phcode.live
Phcode Live Preview Service

Phcode Live Preview Service.

phcode will insert a hidden iframe with the `https://phcode.live` that sets up a
virtual server based on service workers. This repo hosts phcode.live in docs folder.

- This is mainly used in web builds.
- Not used in tauri/native builds. In tauri builds, node based server is used
instead of virtual server.

## How to develope/run locally

By default, phcode.dev will always take the hosted `phcode.live` site instead of
localhost:8001 for live preview hosting. So for development, we need to override
this in [phoenix repo](https://github.com/phcode-dev/phoenix) to this server.

Search for `#LIVE_PREVIEW_STATIC_SERVER_BASE_URL_OVERRIDE` in
[phoenix repo](https://github.com/phcode-dev/phoenix) and follow the steps there
to use this locally hosted live preview server instead of phcode.live.

### on this repo:

- `npm install`
- `npm run serve` will start this live preview server on `localhost:8001`

### After that go to phcode repo

- `npm run serve`
- Go to browser and launch phoenix dev server link http://localhost:8000
- Phoenix will now use this live preview server as live preview dev server.
- Make code changes here as you wish.
- After development, merge in your changes in this repo.

For prod deployment, push the changes to prod branch after thorough testing and
the changes will be automatically deployed to `phcode.live`.
6 changes: 6 additions & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
@@ -3,6 +3,12 @@
<head>
<meta charset="UTF-8">
<title>Live Preview Server Connector</title>
<script src="virtual-server-loader.js" type="module"></script>
<!--
This page should only be loaded once per phcode window and not reloaded for every live preview change.
This server iframe should be reused. Note that different phcode window may still load this page
while another phcode window is serving with this server.
-->
</head>
<body>
Serving Live Preview...
110 changes: 110 additions & 0 deletions docs/pageLoader.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<!DOCTYPE html>

<!--
Copyright (c) 2021 - present core.ai . All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
-->

<html lang="en">

<head>
<title>Phoenix live preview</title>
<style>
.hidden {
display: none;
}
</style>
<script>
const worker = new Worker('pageLoaderWorker.js');
worker.onmessage = (event) => {
const type = event.data.type;
switch (type) {
case 'REDIRECT_PAGE': location.href = event.data.URL; break;
default: console.error("Live Preview page loader: received unknown message from worker:", event);
}
}
window.savePageCtrlSDisabledByPhoenix = true;
addEventListener("keydown", function(e) {
// inside live preview iframe, we disable ctrl-s browser save page dialog
if (e.key === 's' && (navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey)) {
e.preventDefault();
}
}, false);
// do not document.requestStorageAccess as we need an isolated sandboxed
// idb/storage/domain partitin even within live
// preview in same domain
</script>
<script type="module">
const clientID = "" + Math.round( Math.random()*1000000000);
function getExtension(url) {
url = url || '';
let pathSplit = url.split('.');
return pathSplit && pathSplit.length>1 ? pathSplit[pathSplit.length-1] : '';
}
function isImage(url) {
let extension = getExtension(url);
return ["jpg", "jpeg", "png", "gif", "svg", "webp", "bmp", "ico", "avif"]
.includes(extension.toLowerCase());
}
window.handleLoad = function () {
const urlSearchParams = new URLSearchParams(window.location.search);
const params = Object.fromEntries(urlSearchParams.entries());
if(!params.broadcastChannel){
console.error("Live Preview page loader: Could not resolve live preview broadcast channel", params);
return;
}
worker.postMessage({
type: "setupBroadcast",
broadcastChannel: params.broadcastChannel,
clientID});
if(params.URL){
let element = document.getElementById("contentFrame");
if(isImage(decodeURIComponent(params.URL))){
element = document.getElementById("contentImage");
}
element.classList.remove('hidden');
element.src = decodeURIComponent(params.URL);
const path = params.URL.replace(/\/$/, "").split("/");
document.title = path[path.length -1];
}
}

</script>
<style>
body, html
{
margin: 0; padding: 0; height: 100%; overflow: hidden;
}

#content
{
position:absolute; left: 0; right: 0; bottom: 0; top: 0px;
}
</style>
</head>
<body onload="handleLoad()">
<div id="content">
<iframe id="contentFrame" allowfullscreen width="100%" height="100%" frameborder="0"
src="about:blank" title="Live Preview" class="hidden"></iframe>
<img id="contentImage" src="" alt="Image Live Preview" class="hidden"/>
</div>
</body>

</html>
59 changes: 59 additions & 0 deletions docs/pageLoaderWorker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* GNU AGPL-3.0 License
*
* Copyright (c) 2021 - present core.ai . All rights reserved.
*
* This program 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://opensource.org/licenses/AGPL-3.0.
*
*/

/**
* We need to set this up in a worker as if the user is debugging a live preview page, the heartbeat messages will
* be blocked in main thread, but fine in isolated worker thread.
* @private
*/


function _setupBroadcastChannel(broadcastChannel, clientID) {
if(!broadcastChannel){
return;
}
const _livePreviewNavigationChannel=new BroadcastChannel(broadcastChannel);
_livePreviewNavigationChannel.onmessage = (event) => {
const type = event.data.type;
switch (type) {
case 'REDIRECT_PAGE': postMessage({type, URL: event.data.URL}); break;
case 'TAB_ONLINE': break; // do nothing. This is a loopback message from another live preview tab
}
};
function _sendOnlineHeartbeat() {
_livePreviewNavigationChannel.postMessage({
type: 'TAB_ONLINE',
clientID,
URL: location.href
});
}
_sendOnlineHeartbeat();
setInterval(()=>{
_sendOnlineHeartbeat();
}, 1000);
}

onmessage = (event) => {
const type = event.data.type;
switch (type) {
case 'setupBroadcast': _setupBroadcastChannel(event.data.broadcastChannel, event.data.clientID); break;
default: console.error("Live Preview page worker: received unknown event:", event);
}
};
13 changes: 13 additions & 0 deletions docs/trustedOrigins.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const TRUSTED_ORIGINS = {
'http://localhost:8000': true, // phcode dev server
'http://localhost:8001': true, // phcode dev live preview server
'http://localhost:5000': true, // playwright tests
'http://127.0.0.1:8000': true, // phcode dev server
'http://127.0.0.1:8001': true, // phcode dev live preview server
'http://127.0.0.1:5000': true, // playwright tests
'https://phcode.live': true, // phcode prod live preview server
'https://phcode.dev': true,
'https://dev.phcode.dev': true,
'https://staging.phcode.dev': true,
'https://create.phcode.dev': true
};
147 changes: 147 additions & 0 deletions docs/virtual-server-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import {Workbox} from 'https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-window.prod.mjs';
import {TRUSTED_ORIGINS} from "./trustedOrigins.js";

const EVENT_REPORT_ERROR = 'REPORT_ERROR';
const EVENT_SERVER_READY = 'SERVER_READY';
const EVENT_GET_PHOENIX_INSTANCE_ID = 'GET_PHOENIX_INSTANCE_ID';
let PHOENIX_INSTANCE_ID;
const expectedBaseURL = `${location.origin}/`;
const urlParams = new URLSearchParams(window.location.search);
const parentOrigin = urlParams.get('parentOrigin');
if(!parentOrigin){
alert("Missing parentOrigin URL parameter. Embedded server not activated.");
} else if (!TRUSTED_ORIGINS[parentOrigin]) {
alert("This page can only be embedded from trusted domains " + Object.keys(TRUSTED_ORIGINS));
} else {
if(Array.from(urlParams.entries()).length !== 1){
alert("virtual server iframe!! This page should not be loaded with URL params other than parentOrigin.");
}
function _getBaseURL() {
let baseURL = window.location.href;
if(location.href.indexOf( "?")>-1){
baseURL = location.href.substring( 0, location.href.indexOf( "?")); // remove query string params
}
if(location.href.indexOf( "#")>-1){
baseURL = baseURL.substring( 0, baseURL.indexOf( "#")); // remove hrefs in page
}
return baseURL;
}
console.log(_getBaseURL());
function _isServiceWorkerLoaderPage() {
// only http(s)://x.y.z/ can load service worker. http(s)://x.y.z/some/path/cant
// as we will not be able to serve the full dir tree from sub paths.
const currentURL = _getBaseURL();
console.log("currentURL and baseURL:", currentURL, expectedBaseURL);
return currentURL === expectedBaseURL;
}

function post(eventName, message) {
window.parent.postMessage({
handlerName: "ph-liveServer",
eventName,
message
}, parentOrigin);
}

function reportError(message) {
console.error(message);
post(EVENT_REPORT_ERROR, message);
}

const _serverBroadcastChannel = new BroadcastChannel("virtual_server_broadcast");
_serverBroadcastChannel.onmessage = function (event) {
post(event.data.type, event.data)
}
let _livePreviewBroadcastChannel;

// messages sent from phoenix parent window tot his iframe
window.onmessage = function(e) {
// broadcast to service worker/tabs.
if(!TRUSTED_ORIGINS[e.origin]){
console.error("Ignoring post message from untrusted origin " + e.origin +
'. Trusted origins are ' + Object.keys(TRUSTED_ORIGINS));
return;
}
switch (e.data.type) {
case 'REQUEST_RESPONSE':
_serverBroadcastChannel.postMessage(e.data);
break;
case 'PHOENIX_INSTANCE_ID':
PHOENIX_INSTANCE_ID = e.data.PHOENIX_INSTANCE_ID;
let broadcastChannelID = `${PHOENIX_INSTANCE_ID}_livePreview`;
if(!_livePreviewBroadcastChannel) {
_livePreviewBroadcastChannel = new BroadcastChannel(broadcastChannelID);
_livePreviewBroadcastChannel.onmessage = (event) => {
// just pass it on to parent phcode
post(event.data.type, event.data)
};
} else {
console.error("Only one live preview message broadcast channel allowed per iframe. reload page!!!");
}
postIfServerReady();
break;
default:
_livePreviewBroadcastChannel.postMessage(e.data);
break;
}
};

let serviceWorkerLoaded = false;
function postIfServerReady() {
if(serviceWorkerLoaded && PHOENIX_INSTANCE_ID) {
post(EVENT_SERVER_READY, EVENT_SERVER_READY);
}
}
post(EVENT_GET_PHOENIX_INSTANCE_ID, EVENT_GET_PHOENIX_INSTANCE_ID);

function loadServiceWorker() {
if (! 'serviceWorker' in navigator) {
reportError("Live Preivew: Service worker APIs not available. Cannot start virtual server!!!");
return;
}
const wb = new Workbox(`virtual-server-main.js`, {
// https://developer.chrome.com/blog/fresher-sw/#updateviacache
updateViaCache: 'none'
});
function serviceWorkerReady() {
console.log('live preview Service worker loader: Server ready.');
serviceWorkerLoaded = true;
postIfServerReady();
if(!localStorage.getItem("loadedTwice")){
// on first load, we reload the page after service worker is loaded
// so that the site is cached in service worker cache for offline access.
// also on second load, we can guarantee that the page is under control of service worker.
localStorage.setItem("loadedTwice", "true");
location.reload();
}
}

wb.controlling.then(serviceWorkerReady);

// Deal with first-run install, if necessary
wb.addEventListener('installed', (event) => {
if(!event.isUpdate) {
console.log('live preview Service worker loader: Web server Worker installed.');
}
});

// Add an event listener to detect when the registered
// service worker has installed but is waiting to activate.
wb.addEventListener('waiting', (event) => {
console.log("live preview Service worker loader: A new service worker is pending load. Trying to update worker now.");
// Live preview service workers are always updated in phoenix instantly. we dont ask user.
wb.messageSkipWaiting();
});

wb.register();
}

if(!_isServiceWorkerLoaderPage()){
console.log("live preview server: This is not the correct server load URL, redirecting...");
let currentURL = window.location.href;
let queryParams = currentURL.replace(_getBaseURL(),"");
location.href = expectedBaseURL + queryParams;
} else {
loadServiceWorker();
}
}
Loading