[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 492 additions and 392 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>