[CORL-127] Custom CSS (#2194)

* feat: moved html-webpack-plugin to custom server templates in production

* fix: fixed templates

* fix: removed sri for the time being

* fix: fixed up tests for new field name
This commit is contained in:
Wyatt Johnson
2019-02-13 20:45:11 +00:00
committed by GitHub
parent c91a0fafa5
commit aa2346b715
20 changed files with 493 additions and 393 deletions
+111
View File
@@ -0,0 +1,111 @@
import fs from "fs";
import logger from "talk-server/logger";
export interface Asset {
src: string;
integrity: string;
}
/**
* Entrypoint is the version of the entrypoint that has collected entries with
* integrity values.
*/
export type Entrypoint = Record<string, Asset[]>;
/**
* RawEntrypoint is the entrypoint entry generated by the webpack plugin.
*/
export type RawEntrypoint = Record<string, string[]>;
/**
* Manifest is the full raw manifest that is generated by the webpack plugin.
*/
export type Manifest = {
/**
* entrypoints are generated by the webpack plugin for each of the entrypoints
* with their required chunks.
*/
entrypoints: Record<string, RawEntrypoint>;
} & Record<string, Asset>;
/**
* Entrypoints will parse the manifest provided by the `webpack-assets-manifest`
* plugin and make their assets available for each entrypoint.
*/
export default class Entrypoints {
private entrypoints = new Map<string, Entrypoint>();
constructor(manifest: Manifest) {
for (const entry in manifest.entrypoints) {
if (!manifest.entrypoints.hasOwnProperty(entry)) {
continue;
}
// Grab the entrypoint that contains the list of all the assets
// for this entrypoint.
const entrypoint: Entrypoint = {};
// Itterate over the extension's in the entrypoint.
for (const extension in manifest.entrypoints[entry]) {
if (!manifest.entrypoints[entry].hasOwnProperty(extension)) {
continue;
}
// Create the extension in the entrypoint.
entrypoint[extension] = [];
// Grab the files in the extension.
const assets = manifest.entrypoints[entry][extension];
// Itterate over the src field for each of the files.
for (const src of assets) {
// Search for the entry in the assets.
for (const name in manifest) {
if (name !== "entrypoints" && !manifest.hasOwnProperty(name)) {
continue;
}
// Grab the asset.
const asset = manifest[name];
// Check to see if the asset is a match.
if (asset.src === src) {
entrypoint[extension].push({
integrity: asset.integrity,
// Prefix all the sources with a `/`.
src: "/" + asset.src,
});
break;
}
}
}
this.entrypoints.set(entry, entrypoint);
}
}
}
public get(name: string): Readonly<Entrypoint> {
const entrypoint = this.entrypoints.get(name);
if (!entrypoint) {
throw new Error(`Entrypoint ${name} does not exist in the manifest`);
}
return entrypoint;
}
public static fromFile(filepath: string): Entrypoints | null {
try {
// Load the manifest.
const manifest = JSON.parse(
fs.readFileSync(filepath, { encoding: "utf8" })
);
// Create and return the entrypoints.
return new Entrypoints(manifest);
} catch (err) {
logger.error({ err }, "could not load the manifest");
return null;
}
}
}
+2 -13
View File
@@ -102,15 +102,8 @@ function configureApplication(options: AppOptions) {
function setupViews(options: AppOptions) {
const { parent } = options;
// Configure the default views directories.
const views = [
// Load the templates compiled by Webpack.
path.resolve(
path.join(__dirname, "..", "..", "..", "..", "dist", "static")
),
// Load the templates generated by the server.
path.join(__dirname, "views"),
];
// Configure the default views directory.
const views = path.join(__dirname, "views");
parent.set("views", views);
// Reconfigure nunjucks.
@@ -119,13 +112,9 @@ function setupViews(options: AppOptions) {
// caching.
watch: options.config.get("env") === "development",
noCache: options.config.get("env") === "development",
// Trim blocks of whitespace.
trimBlocks: true,
lstripBlocks: true,
});
// assign the nunjucks engine to .njk and .html files.
parent.engine("njk", cons.nunjucks);
parent.engine("html", cons.nunjucks);
// set .html as the default extension.
+31 -4
View File
@@ -1,12 +1,20 @@
import express from "express";
import { minify } from "html-minifier";
import { cacheHeadersMiddleware } from "talk-server/app/middleware/cacheHeaders";
import { Entrypoint } from "../helpers/entrypoints";
export interface ClientTargetHandlerOptions {
/**
* view is the name of the template to render.
* entrypoint is the entrypoint entry to load.
*/
view: string;
entrypoint: Entrypoint;
/**
* enableCustomCSS will insert the custom CSS into the template if it is
* available on the Tenant.
*/
enableCustomCSS?: boolean;
/**
* cacheDuration is the cache duration that a given request should be cached for.
@@ -22,7 +30,8 @@ export interface ClientTargetHandlerOptions {
export function createClientTargetRouter({
staticURI,
view,
entrypoint,
enableCustomCSS = false,
cacheDuration = "1h",
}: ClientTargetHandlerOptions) {
// Create a router.
@@ -32,7 +41,25 @@ export function createClientTargetRouter({
router.use(cacheHeadersMiddleware(cacheDuration));
// Wildcard display all the client routes under the provided prefix.
router.get("/*", (req, res) => res.render(view, { staticURI }));
router.get("/*", (req, res, next) =>
res.render(
"client",
{ staticURI, entrypoint, enableCustomCSS },
(err, html) => {
if (err) {
return next(err);
}
// Send back the HTML minified.
res.send(
minify(html, {
removeComments: true,
collapseWhitespace: true,
})
);
}
)
);
return router;
}
+82 -58
View File
@@ -1,4 +1,5 @@
import express, { Router } from "express";
import path from "path";
import { AppOptions } from "talk-server/app";
import { noCacheMiddleware } from "talk-server/app/middleware/cacheHeaders";
@@ -9,6 +10,7 @@ import logger from "talk-server/logger";
import { cspTenantMiddleware } from "talk-server/app/middleware/csp/tenant";
import { tenantMiddleware } from "talk-server/app/middleware/tenant";
import Entrypoints from "../helpers/entrypoints";
import { createAPIRouter } from "./api";
import { createClientTargetRouter } from "./client";
@@ -28,67 +30,89 @@ export async function createRouter(app: AppOptions, options: RouterOptions) {
const staticURI = app.config.get("static_uri");
// Add the embed targets.
router.use(
"/embed/stream",
createClientTargetRouter({
staticURI,
view: "stream",
})
);
router.use(
"/embed/auth",
createClientTargetRouter({
staticURI,
view: "auth",
cacheDuration: false,
})
);
router.use(
"/embed/auth/callback",
createClientTargetRouter({
staticURI,
view: "auth-callback",
cacheDuration: false,
})
// Load the entrypoint manifest.
const manifest = path.join(
__dirname,
"..",
"..",
"..",
"..",
"..",
"dist",
"static",
"asset-manifest.json"
);
const entrypoints = Entrypoints.fromFile(manifest);
// Add the standalone targets.
router.use(
"/admin",
// If we aren't already installed, redirect the user to the install page.
installedMiddleware({
tenantCache: app.tenantCache,
}),
createClientTargetRouter({
staticURI,
view: "admin",
cacheDuration: false,
})
);
router.use(
"/install",
// If we're already installed, redirect the user to the admin page.
installedMiddleware({
redirectIfInstalled: true,
redirectURL: "/admin",
tenantCache: app.tenantCache,
}),
createClientTargetRouter({
staticURI,
view: "install",
cacheDuration: false,
})
);
if (entrypoints) {
// Add the embed targets.
router.use(
"/embed/stream",
createClientTargetRouter({
staticURI,
enableCustomCSS: true,
entrypoint: entrypoints.get("stream"),
})
);
router.use(
"/embed/auth",
createClientTargetRouter({
staticURI,
cacheDuration: false,
entrypoint: entrypoints.get("auth"),
})
);
router.use(
"/embed/auth/callback",
createClientTargetRouter({
staticURI,
cacheDuration: false,
entrypoint: entrypoints.get("authCallback"),
})
);
// Handle the root path.
router.get(
"/",
// Redirect the user to the install page if they are not, otherwise redirect
// them to the admin.
installedMiddleware({ tenantCache: app.tenantCache }),
(req, res, next) => res.redirect("/admin")
);
// Add the standalone targets.
router.use(
"/admin",
// If we aren't already installed, redirect the user to the install page.
installedMiddleware({
tenantCache: app.tenantCache,
}),
createClientTargetRouter({
staticURI,
cacheDuration: false,
entrypoint: entrypoints.get("admin"),
})
);
router.use(
"/install",
// If we're already installed, redirect the user to the admin page.
installedMiddleware({
redirectIfInstalled: true,
redirectURL: "/admin",
tenantCache: app.tenantCache,
}),
createClientTargetRouter({
staticURI,
cacheDuration: false,
entrypoint: entrypoints.get("install"),
})
);
// Handle the root path.
router.get(
"/",
// Redirect the user to the install page if they are not, otherwise redirect
// them to the admin.
installedMiddleware({ tenantCache: app.tenantCache }),
(req, res, next) => res.redirect("/admin")
);
} else {
logger.warn(
{ manifest },
"could not load the generated manifest, client routes will remain un-mounted"
);
}
return router;
}
+37
View File
@@ -0,0 +1,37 @@
{% import "macros.html" as macros %}
{% extends "templates/base.html" %}
{% block title %}Talk{% endblock %}
{% block meta %}
<script type="application/javascript" id="config">{{ staticURI | dump | safe }}</script>
{% endblock %}
{# Include all the styles from the entrypoint #}
{% if entrypoint.css or enableCustomCSS %}
{% block css %}
{% if entrypoint.css %}
{% for asset in entrypoint.css %}
{{ macros.css(asset.src, asset.integrity, staticURI) }}
{% endfor %}
{% endif %}
{% if enableCustomCSS %}
{# Custom CSS is included after the CSS block so that its overrides will apply #}
{% include "partials/customCSS.html" %}
{% endif %}
{% endblock %}
{% endif %}
{% block html %}
<div id="app"></div>
{% endblock %}
{# Include all the scripts from the entrypoint #}
{% if entrypoint.js %}
{% block js %}
{% for asset in entrypoint.js %}
{{ macros.js(asset.src, asset.integrity, staticURI) }}
{% endfor %}
{% endblock %}
{% endif %}
+19
View File
@@ -0,0 +1,19 @@
{% macro css(src, integrity = '', prefix = '') %}
<link type="text/css" rel="stylesheet" href="{{ prefix }}{{ src }}"/>
{# TODO: evaluate when to enable SRI, non-SSL connections cause issues #}
{# {% if integrity %}
<link type="text/css" rel="stylesheet" href="{{ prefix }}{{ src }}" integrity="{{ integrity }}" crossorigin="anonymous"/>
{% else %}
<link type="text/css" rel="stylesheet" href="{{ prefix }}{{ src }}"/>
{% endif %} #}
{% endmacro %}
{% macro js(src, integrity = '', prefix = '') %}
<script type="application/javascript" src="{{ prefix }}{{ src }}"></script>
{# TODO: evaluate when to enable SRI, non-SSL connections cause issues #}
{# {% if false %}
<script type="application/javascript" src="{{ prefix }}{{ src }}" integrity="{{ integrity }}" crossorigin="anonymous"></script>
{% else %}
<script type="application/javascript" src="{{ prefix }}{{ src }}"></script>
{% endif %} #}
{% endmacro %}
@@ -0,0 +1,5 @@
{% import "../macros.html" as macros %}
{% if tenant and tenant.customCSSURL %}
{{ macros.css(tenant.customCSSURL) }}
{% endif %}
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
{# Meta tags #}
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no" />
{% block meta %}{% endblock %}
{# Title #}
<title>{% block title %}{% endblock %}</title>
{# CSS #}
{% block css %}{% endblock %}
</head>
<body>
{% block body %}
{% block html %}
<div id="root"></div>
{% endblock %}
{% endblock %}
{% block js %}{% endblock %}
</body>
</html>
@@ -935,9 +935,9 @@ type Settings {
autoCloseStream: Boolean! @auth(roles: [ADMIN])
"""
customCssUrl is the URL of the custom CSS used to display on the frontend.
customCSSURL is the URL of the custom CSS used to display on the frontend.
"""
customCssUrl: String
customCSSURL: String
"""
closedTimeout is the amount of time (in seconds) from the createdAt timestamp
@@ -2201,9 +2201,9 @@ input SettingsInput {
autoCloseStream: Boolean
"""
customCssUrl is the URL of the custom CSS used to display on the frontend.
customCSSURL is the URL of the custom CSS used to display on the frontend.
"""
customCssUrl: String
customCSSURL: String
"""
closedTimeout is the amount of seconds from the createdAt timestamp that a
+5 -1
View File
@@ -80,7 +80,11 @@ export interface Auth {
}
export interface Settings extends ModerationSettings {
customCssUrl?: string;
/**
* customCSSURL is the URL that can be specified by the Tenant to describe a
* URL that contains custom styles to be applied to the Stream.
*/
customCSSURL?: string;
/**
* editCommentWindowLength is the length of time (in seconds) after a comment