Skip to content

Commit

Permalink
Merge pull request #45 from floodfx/extract_handler
Browse files Browse the repository at this point in the history
Extract handler
  • Loading branch information
floodfx authored Mar 7, 2022
2 parents 91097c3 + 14cd0eb commit 8a5808f
Show file tree
Hide file tree
Showing 10 changed files with 379 additions and 205 deletions.
325 changes: 170 additions & 155 deletions coverage/clover.xml

Large diffs are not rendered by default.

39 changes: 20 additions & 19 deletions coverage/coverage-final.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "liveviewjs",
"version": "0.0.10",
"version": "0.0.11",
"description": "LiveViewJS brings the power of Phoenix LiveView to Typescript and Javascript developers and applications.",
"targets": {
"client": {
Expand Down
17 changes: 2 additions & 15 deletions src/examples/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import path from 'path';
import { LiveViewServer } from '../server';
import { LiveViewRouter } from '../server/component/types';
import { configLiveViewHandler } from '../server/live_view_route';
import { AsyncFetchLiveViewComponent } from './asyncfetch/component';
import { AutocompleteLiveViewComponent } from './autocomplete/component';
import { LicenseLiveViewComponent } from './license_liveview';
Expand Down Expand Up @@ -35,6 +34,8 @@ const lvServer = new LiveViewServer({

const router: LiveViewRouter = {
"/light": new LightLiveViewComponent(),
// sub paths also work
"/foo/light": new LightLiveViewComponent(),
"/license": new LicenseLiveViewComponent(),
'/sales-dashboard': new SalesDashboardLiveViewComponent(),
'/search': new SearchLiveViewComponent(),
Expand All @@ -52,20 +53,6 @@ lvServer.registerLiveViewRoutes(router)
// register single route
// lvServer.registerLiveViewRoute("/volunteers", new VolunteerComponent())


lvServer.expressApp.get("/foo/bar", configLiveViewHandler(
new LightLiveViewComponent(),
"root.html.ejs",
"signing-secret-foo",
(req) => {
return { ...req.session, csrfToken: "csrfToken"}
},
{
title: "Examples",
},
)
)

// add your own routes to the express app
lvServer.expressApp.get("/", (req, res) => {

Expand Down
6 changes: 4 additions & 2 deletions src/server/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export * from "./live_view_server";
export * from "./component";
export * from "./live_view_route";
export * from "./live_view_server";
export * from "./templates";
export * from "./templates/helpers";
export * from "./templates/diff";
export * from "./templates/helpers";

93 changes: 93 additions & 0 deletions src/server/live_view_route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { SessionData } from "express-session";
import { nanoid } from "nanoid";
import request from "superwstest";
import { configLiveViewHandler } from ".";
import { BaseLiveViewComponent } from "./component/base_component";
import { LiveViewMountParams, LiveViewSocket } from "./component/types";
import { LiveViewServer } from "./live_view_server";
import { html } from "./templates";


describe("test live view route", () => {

it("starting / stopping twice has no effect", async () => {
const lvServer = new LiveViewServer({
signingSecret: "MY_VERY_SECRET_KEY",
port: 7878,
pageTitleDefaults: {
prefix: "TitlePrefix - ",
suffix: " - TitleSuffix",
title: "Title",
},
});
lvServer.start();
expect(lvServer.isStarted).toBe(true);
lvServer.start();
expect(lvServer.isStarted).toBe(true);
lvServer.shutdown();
expect(lvServer.isStarted).toBe(false);
lvServer.shutdown();
expect(lvServer.isStarted).toBe(false);
})

it("use configLiveViewHandler", (done) => {
const lvServer = new LiveViewServer({
signingSecret: "MY_VERY_SECRET_KEY",
port: 7878,
pageTitleDefaults: {
prefix: "TitlePrefix - ",
suffix: " - TitleSuffix",
title: "Title",
},
});

lvServer.start();
expect(lvServer.isStarted).toBe(true);
const lvComponent = new LiveViewComponent()
lvServer.expressApp.get(...configLiveViewHandler(
"/test/foo",
lvComponent,
"root.html.ejs",
"my signing secret",
(req) => {
return {
...req.session, // copy session data
csrfToken: req.session.csrfToken || nanoid(),
}
},
{
title: "Default Title"
}
))
lvServer.start()
setTimeout((() => {
request(lvServer.httpServer).get('/test/foo').expect(200).then(res => {
expect(res.text).toContain(lvComponent.render({ message: "test" }).toString())
done();
lvServer.shutdown()
})
}), 100)
})



})

declare module 'express-session' {
interface SessionData {
message: string;
}
}

class LiveViewComponent extends BaseLiveViewComponent<{ message?: string }, {}> {

mount(params: LiveViewMountParams, session: Partial<SessionData>, socket: LiveViewSocket<{}>): {} {
return { message: session.message || "test" }
}

render(ctx: { message: string }) {
const { message } = ctx
return html`<div>${message}</div>`;
}

}
62 changes: 62 additions & 0 deletions src/server/live_view_route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { NextFunction, Request, Response } from "express";
import jwt from "jsonwebtoken";
import { nanoid } from "nanoid";
import { LiveViewComponent, LiveViewSocket, PageTitleDefaults } from ".";

type SessionDataProvider<T extends {csrfToken: string}> = (req: Request) => T;

const emptyVoid = () => {};

export const configLiveViewHandler = <T extends {csrfToken: string}>(
getPath: string,
component: LiveViewComponent<unknown,unknown>,
rootView: string,
signingSecret: string,
sessionDataProvider: SessionDataProvider<T>,
pageTitleDefaults: PageTitleDefaults
): [string, (req:Request, res: Response, next: NextFunction) => Promise<void>] => {
return [getPath, async (req:Request, res: Response, next: NextFunction) => {

// new LiveViewId for each request
const liveViewId = nanoid();

// mock socket
const liveViewSocket: LiveViewSocket<T> = {
id: liveViewId,
connected: false, // ws socket not connected on http request
context: {} as T,
sendInternal: emptyVoid,
repeat: emptyVoid,
pageTitle: emptyVoid,
subscribe: emptyVoid,
pushPatch: emptyVoid,
}

// get session data from provider
const session = sessionDataProvider(req);

// mount and render component
const ctx = await component.mount(
{
_csrf_token: session.csrfToken,
_mounts: -1
},
session,
liveViewSocket
);
const view = component.render(ctx);

// render the view with all the data
res.render(rootView, {
page_title: pageTitleDefaults?.title ?? "",
page_title_prefix: pageTitleDefaults?.prefix,
page_title_suffix: pageTitleDefaults?.suffix,
csrf_meta_tag: req.session.csrfToken,
liveViewId,
session: jwt.sign(session, signingSecret),
// TODO support static assets https://github.com/floodfx/liveviewjs/issues/42
statics: jwt.sign(JSON.stringify(view.statics), signingSecret),
inner_content: view.toString()
})
}]
}
19 changes: 14 additions & 5 deletions src/server/live_view_server.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { SessionData } from "express-session";
import { LiveViewServer } from "./live_view_server";
import { html } from "./templates";
import { LiveViewMountParams, LiveViewRouter, LiveViewSocket } from "./component/types";
import request from "superwstest"
import { Server } from "http";
import { PhxJoinIncoming } from "./socket/types";
import request from "superwstest";
import { BaseLiveViewComponent } from "./component/base_component";
import { LiveViewMountParams, LiveViewRouter, LiveViewSocket } from "./component/types";
import { LiveViewServer } from "./live_view_server";
import { PhxJoinIncoming } from "./socket/types";
import { html } from "./templates";


describe("test live view server", () => {
Expand Down Expand Up @@ -71,6 +71,15 @@ describe("test live view server", () => {
})
})

it("http request renders a live view component html at deeper path", (done) => {
const lvComponent = new LiveViewComponent()
lvServer.registerLiveViewRoute("/test/foo/bar", lvComponent)
request(lvServer.httpServer).get('/test/foo/bar').expect(200).then(res => {
expect(res.text).toContain(lvComponent.render({ message: "test" }).toString())
done();
})
})

it("http request contains live title components", (done) => {
const lvComponent = new LiveViewComponent()
lvServer.registerLiveViewRoute("/test", lvComponent)
Expand Down
19 changes: 11 additions & 8 deletions src/server/live_view_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,13 @@ export class LiveViewServer {
this.httpServer.close();
}


private buildExpressApp() {
const app = express();

// empty void function
const emptyVoid = () => {};

app.use(express.static(this.publicPath))

app.set('view engine', 'ejs');
Expand All @@ -153,10 +157,9 @@ export class LiveViewServer {
// register live_title_tag helper
app.locals.live_title_tag = live_title_tag;

app.get('/:liveview', async (req, res, next) => {
const liveview = req.params.liveview;

const emptyVoid = () => { };
// handle all views and look up components by path
app.use(async (req, res, next) => {
const liveview = req.path;

// new LiveViewId per HTTP requess?
const liveViewId = nanoid();
Expand All @@ -172,7 +175,7 @@ export class LiveViewServer {
}

// look up component for route
const component = this._router[`/${liveview}`];
const component = this._router[liveview];
if (!component) {
// no component found for route so call next() to
// let a possible downstream route handle the request
Expand All @@ -185,15 +188,15 @@ export class LiveViewServer {
req.session.csrfToken = nanoid();
}

const jwtPayload: Omit<SessionData, "cookie"> = {
const sessionData: Omit<SessionData, "cookie" | "message"> = {
...req.session,
csrfToken: req.session.csrfToken,
}

// mount and render component if found
const ctx = await component.mount(
{ _csrf_token: req.session.csrfToken, _mounts: -1 },
{ ...jwtPayload },
{ ...sessionData },
liveViewSocket
);
const view = component.render(ctx);
Expand All @@ -205,7 +208,7 @@ export class LiveViewServer {
page_title_suffix: this.pageTitleDefaults?.suffix,
csrf_meta_tag: req.session.csrfToken,
liveViewId,
session: jwt.sign(jwtPayload, this.signingSecret),
session: jwt.sign(sessionData, this.signingSecret),
statics: jwt.sign(JSON.stringify(view.statics), this.signingSecret),
inner_content: view.toString()
})
Expand Down
2 changes: 2 additions & 0 deletions src/server/pubsub/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ export interface Publisher<T> {
broadcast(topic: string, data: T): void;
}

export * from "./SingleProcessPubSub"

0 comments on commit 8a5808f

Please sign in to comment.