mirror of
https://github.com/wassname/talk.git
synced 2026-07-15 11:26:58 +08:00
[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:
@@ -1,7 +1,7 @@
|
||||
import OptimizeCssnanoPlugin from "@intervolga/optimize-cssnano-plugin";
|
||||
import CaseSensitivePathsPlugin from "case-sensitive-paths-webpack-plugin";
|
||||
import CompressionPlugin from "compression-webpack-plugin";
|
||||
import HtmlWebpackPlugin, { Options } from "html-webpack-plugin";
|
||||
import HtmlWebpackPlugin from "html-webpack-plugin";
|
||||
import { identity } from "lodash";
|
||||
import MiniCssExtractPlugin from "mini-css-extract-plugin";
|
||||
import path from "path";
|
||||
@@ -9,14 +9,12 @@ import WatchMissingNodeModulesPlugin from "react-dev-utils/WatchMissingNodeModul
|
||||
import TerserPlugin from "terser-webpack-plugin";
|
||||
import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin";
|
||||
import webpack, { Configuration, Plugin } from "webpack";
|
||||
import WebpackAssetsManifest from "webpack-assets-manifest";
|
||||
import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer";
|
||||
import ManifestPlugin from "webpack-manifest-plugin";
|
||||
|
||||
import { Config } from "./config";
|
||||
import { createClientEnv } from "./config";
|
||||
import paths from "./paths";
|
||||
import InterpolateHtmlPlugin from "./plugins/InterpolateHtmlPlugin";
|
||||
import PublicURIWebpackPlugin from "./plugins/PublicURIWebpackPlugin";
|
||||
|
||||
/**
|
||||
* filterPlugins will filter out null values from the array of plugins, allowing
|
||||
@@ -59,23 +57,16 @@ export default function createWebpackConfig(
|
||||
* ifProduction will only include the nodes if we're in production mode.
|
||||
*/
|
||||
const ifProduction = isProduction
|
||||
? <T extends {}>(...nodes: T[]) => nodes
|
||||
: <T extends {}>(...nodes: T[]) => [];
|
||||
? (...nodes: any[]) => nodes
|
||||
: (...nodes: any[]) => [];
|
||||
|
||||
const htmlWebpackConfig: Options = {
|
||||
minify: minimize && {
|
||||
removeComments: true,
|
||||
collapseWhitespace: true,
|
||||
removeRedundantAttributes: true,
|
||||
useShortDoctype: true,
|
||||
removeEmptyAttributes: true,
|
||||
removeStyleLinkTypeAttributes: true,
|
||||
keepClosingSlash: true,
|
||||
minifyJS: true,
|
||||
minifyCSS: true,
|
||||
minifyURLs: true,
|
||||
},
|
||||
};
|
||||
/**
|
||||
* ifNotProduction will only include the nodes if we're not in production
|
||||
* mode.
|
||||
*/
|
||||
const ifNotProduction = !isProduction
|
||||
? (...nodes: any[]) => nodes
|
||||
: (...nodes: any[]) => [];
|
||||
|
||||
const styleLoader = {
|
||||
loader: require.resolve("style-loader"),
|
||||
@@ -532,66 +523,50 @@ export default function createWebpackConfig(
|
||||
},
|
||||
plugins: filterPlugins([
|
||||
...baseConfig.plugins!,
|
||||
// Generates an `stream.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "stream.html",
|
||||
template: paths.appStreamHTML,
|
||||
chunks: ["stream"],
|
||||
inject: "body",
|
||||
...htmlWebpackConfig,
|
||||
}),
|
||||
// Generates an `auth.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "auth.html",
|
||||
template: paths.appAuthHTML,
|
||||
chunks: ["auth"],
|
||||
inject: "body",
|
||||
...htmlWebpackConfig,
|
||||
}),
|
||||
// Generates an `auth-callback.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "auth-callback.html",
|
||||
template: paths.appAuthCallbackHTML,
|
||||
chunks: ["authCallback"],
|
||||
inject: "body",
|
||||
...htmlWebpackConfig,
|
||||
}),
|
||||
// Generates an `install.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "install.html",
|
||||
template: paths.appInstallHTML,
|
||||
chunks: ["install"],
|
||||
inject: "body",
|
||||
...htmlWebpackConfig,
|
||||
}),
|
||||
// Generates an `admin.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "admin.html",
|
||||
template: paths.appAdminHTML,
|
||||
chunks: ["admin"],
|
||||
inject: "body",
|
||||
...htmlWebpackConfig,
|
||||
}),
|
||||
...ifNotProduction(
|
||||
// Generates an `stream.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "stream.html",
|
||||
template: paths.appStreamHTML,
|
||||
chunks: ["stream"],
|
||||
inject: "body",
|
||||
}),
|
||||
// Generates an `auth.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "auth.html",
|
||||
template: paths.appAuthHTML,
|
||||
chunks: ["auth"],
|
||||
inject: "body",
|
||||
}),
|
||||
// Generates an `auth-callback.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "auth-callback.html",
|
||||
template: paths.appAuthCallbackHTML,
|
||||
chunks: ["authCallback"],
|
||||
inject: "body",
|
||||
}),
|
||||
// Generates an `install.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "install.html",
|
||||
template: paths.appInstallHTML,
|
||||
chunks: ["install"],
|
||||
inject: "body",
|
||||
}),
|
||||
// Generates an `admin.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "admin.html",
|
||||
template: paths.appAdminHTML,
|
||||
chunks: ["admin"],
|
||||
inject: "body",
|
||||
})
|
||||
),
|
||||
...ifProduction(
|
||||
// Inject the pieces we need here to resolve all the now relative url's
|
||||
// against the CDN if it's provided. It will inject the following into
|
||||
// the configuration blob on the page.
|
||||
new PublicURIWebpackPlugin(
|
||||
"{{ staticURI | dump | safe }}",
|
||||
"{{ staticURI }}"
|
||||
)
|
||||
new WebpackAssetsManifest({
|
||||
output: "asset-manifest.json",
|
||||
entrypoints: true,
|
||||
integrity: true,
|
||||
})
|
||||
),
|
||||
// Makes some environment variables available in index.html.
|
||||
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
|
||||
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
// In development, this will be an empty string.
|
||||
new InterpolateHtmlPlugin(env),
|
||||
// Generate a manifest file which contains a mapping of all asset filenames
|
||||
// to their corresponding output file so that tools can pick it up without
|
||||
// having to parse `index.html`.
|
||||
new ManifestPlugin({
|
||||
fileName: "asset-manifest.json",
|
||||
}),
|
||||
]),
|
||||
},
|
||||
/* Webpack config for our embed */
|
||||
@@ -622,40 +597,31 @@ export default function createWebpackConfig(
|
||||
},
|
||||
plugins: filterPlugins([
|
||||
...baseConfig.plugins!,
|
||||
...(isProduction
|
||||
? []
|
||||
: [
|
||||
// Generates an `embed.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "embed.html",
|
||||
template: paths.appEmbedHTML,
|
||||
inject: "head",
|
||||
...htmlWebpackConfig,
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "story.html",
|
||||
template: paths.appEmbedStoryHTML,
|
||||
inject: "head",
|
||||
...htmlWebpackConfig,
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "storyButton.html",
|
||||
template: paths.appEmbedStoryButtonHTML,
|
||||
inject: "head",
|
||||
...htmlWebpackConfig,
|
||||
}),
|
||||
// Makes some environment variables available in index.html.
|
||||
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
|
||||
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
// In development, this will be an empty string.
|
||||
new InterpolateHtmlPlugin(env),
|
||||
]),
|
||||
// Generate a manifest file which contains a mapping of all asset filenames
|
||||
// to their corresponding output file so that tools can pick it up without
|
||||
// having to parse `index.html`.
|
||||
new ManifestPlugin({
|
||||
fileName: "embed-manifest.json",
|
||||
}),
|
||||
...ifNotProduction(
|
||||
// Generates an `embed.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "embed.html",
|
||||
template: paths.appEmbedHTML,
|
||||
inject: "head",
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "story.html",
|
||||
template: paths.appEmbedStoryHTML,
|
||||
inject: "head",
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "storyButton.html",
|
||||
template: paths.appEmbedStoryButtonHTML,
|
||||
inject: "head",
|
||||
})
|
||||
),
|
||||
...ifProduction(
|
||||
new WebpackAssetsManifest({
|
||||
output: "embed-asset-manifest.json",
|
||||
entrypoints: true,
|
||||
integrity: true,
|
||||
})
|
||||
),
|
||||
]),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
// This Webpack plugin lets us interpolate custom variables into `index.html`.
|
||||
// Usage: `new InterpolateHtmlPlugin({ 'MY_VARIABLE': 42 })`
|
||||
// Then, you can use %MY_VARIABLE% in your `index.html`.
|
||||
|
||||
// It works in tandem with HtmlWebpackPlugin.
|
||||
// Learn more about creating plugins like this:
|
||||
// https://github.com/ampedandwired/html-webpack-plugin#events
|
||||
|
||||
import escapeStringRegexp from "escape-string-regexp";
|
||||
import HtmlWebpackPlugin from "html-webpack-plugin";
|
||||
import { Compiler, Plugin } from "webpack";
|
||||
|
||||
export default class InterpolateHtmlPlugin implements Plugin {
|
||||
private replacements: Record<string, string>;
|
||||
|
||||
constructor(replacements: Record<string, string>) {
|
||||
this.replacements = replacements;
|
||||
}
|
||||
|
||||
public apply(compiler: Compiler) {
|
||||
// The following was modified from the original source:
|
||||
// https://github.com/jussikinnula/react-dev-utils/blob/9c290281877c026774c909b2900000c653f431a4/InterpolateHtmlPlugin.js
|
||||
// to support Webpack 4.
|
||||
compiler.hooks.compilation.tap("InterpolateHtmlPlugin", compilation => {
|
||||
const hooks = (HtmlWebpackPlugin as any).getHooks(compilation);
|
||||
hooks.afterTemplateExecution.tap("InterpolateHtmlPlugin", (data: any) => {
|
||||
// Run HTML through a series of user-specified string replacements.
|
||||
Object.keys(this.replacements).forEach(key => {
|
||||
const value = this.replacements[key];
|
||||
data.html = data.html.replace(
|
||||
new RegExp("%" + escapeStringRegexp(key) + "%", "g"),
|
||||
value
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import HtmlWebpackPlugin from "html-webpack-plugin";
|
||||
import { AsyncSeriesWaterfallHook } from "tapable";
|
||||
import { Compiler, Plugin } from "webpack";
|
||||
|
||||
// Copied from @types/html-webpack-plugin
|
||||
interface HtmlTagObject {
|
||||
/**
|
||||
* Attributes of the html tag
|
||||
* E.g. `{'disabled': true, 'value': 'demo'}`
|
||||
*/
|
||||
attributes: {
|
||||
[attributeName: string]: string | boolean;
|
||||
};
|
||||
/**
|
||||
* Wether this html must not contain innerHTML
|
||||
* @see https://www.w3.org/TR/html5/syntax.html#void-elements
|
||||
*/
|
||||
voidTag: boolean;
|
||||
/**
|
||||
* The tag name e.g. `'div'`
|
||||
*/
|
||||
tagName: string;
|
||||
/**
|
||||
* Inner HTML The
|
||||
*/
|
||||
innerHTML?: string;
|
||||
}
|
||||
|
||||
type AlterAssetTagGroupsHook = AsyncSeriesWaterfallHook<{
|
||||
headTags: Array<HtmlTagObject | HtmlTagObject>;
|
||||
bodyTags: Array<HtmlTagObject | HtmlTagObject>;
|
||||
outputName: string;
|
||||
plugin: HtmlWebpackPlugin;
|
||||
}>;
|
||||
|
||||
export default class PublicURIWebpackPlugin implements Plugin {
|
||||
private configTemplate: string;
|
||||
private prefixTemplate: string;
|
||||
|
||||
constructor(configTemplate: string, prefixTemplate: string) {
|
||||
this.configTemplate = configTemplate;
|
||||
this.prefixTemplate = prefixTemplate;
|
||||
}
|
||||
|
||||
private prefixAttribute(attr: string | boolean) {
|
||||
if (!attr || typeof attr !== "string" || !attr.startsWith("/")) {
|
||||
return attr;
|
||||
}
|
||||
|
||||
return this.prefixTemplate + attr;
|
||||
}
|
||||
|
||||
private prefixTag = (tag: {
|
||||
tagName: string;
|
||||
attributes: Record<string, string | boolean>;
|
||||
}) => {
|
||||
switch (tag.tagName) {
|
||||
case "link":
|
||||
tag.attributes.href = this.prefixAttribute(tag.attributes.href);
|
||||
break;
|
||||
case "script":
|
||||
tag.attributes.src = this.prefixAttribute(tag.attributes.src);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
public apply = (compiler: Compiler) => {
|
||||
compiler.hooks.compilation.tap("PublicURIWebpackPlugin", compilation => {
|
||||
const hooks = (HtmlWebpackPlugin as any).getHooks(compilation);
|
||||
(hooks.alterAssetTagGroups as AlterAssetTagGroupsHook).tapAsync(
|
||||
"PublicURIWebpackPlugin",
|
||||
(htmlPluginData, cb) => {
|
||||
// Prefix all the asset's url's with the template.
|
||||
htmlPluginData.headTags.forEach(this.prefixTag);
|
||||
htmlPluginData.bodyTags.forEach(this.prefixTag);
|
||||
|
||||
// Insert the public path reference.
|
||||
htmlPluginData.bodyTags.unshift({
|
||||
tagName: "script",
|
||||
attributes: {
|
||||
type: "application/json",
|
||||
id: "config",
|
||||
},
|
||||
innerHTML: this.configTemplate,
|
||||
voidTag: false,
|
||||
});
|
||||
|
||||
return cb(null, htmlPluginData);
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
+2
-2
@@ -20,7 +20,7 @@ const CustomCSSConfig: StatelessComponent<Props> = ({ disabled }) => (
|
||||
<FormField>
|
||||
<HorizontalGutter size="full">
|
||||
<Localized id="configure-advanced-customCSS">
|
||||
<Header container={<label htmlFor="configure-advanced-customCssUrl" />}>
|
||||
<Header container={<label htmlFor="configure-advanced-customCSSURL" />}>
|
||||
Custom CSS
|
||||
</Header>
|
||||
</Localized>
|
||||
@@ -33,7 +33,7 @@ const CustomCSSConfig: StatelessComponent<Props> = ({ disabled }) => (
|
||||
styles. Can be internal or external.
|
||||
</Typography>
|
||||
</Localized>
|
||||
<Field name="customCssUrl">
|
||||
<Field name="customCSSURL">
|
||||
{({ input, meta }) => (
|
||||
<>
|
||||
<TextField
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ class CustomCSSConfigContainer extends React.Component<Props> {
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
settings: graphql`
|
||||
fragment CustomCSSConfigContainer_settings on Settings {
|
||||
customCssUrl
|
||||
customCSSURL
|
||||
}
|
||||
`,
|
||||
})(CustomCSSConfigContainer);
|
||||
|
||||
@@ -115,7 +115,7 @@ exports[`renders configure advanced 1`] = `
|
||||
>
|
||||
<label
|
||||
className="Typography-root Typography-heading1 Typography-colorTextPrimary Header-root"
|
||||
htmlFor="configure-advanced-customCssUrl"
|
||||
htmlFor="configure-advanced-customCSSURL"
|
||||
>
|
||||
Custom CSS
|
||||
</label>
|
||||
@@ -133,8 +133,8 @@ exports[`renders configure advanced 1`] = `
|
||||
autoCorrect="off"
|
||||
className="TextField-input TextField-colorRegular"
|
||||
disabled={false}
|
||||
id="configure-advanced-customCssUrl"
|
||||
name="customCssUrl"
|
||||
id="configure-advanced-customCSSURL"
|
||||
name="customCSSURL"
|
||||
onChange={[Function]}
|
||||
placeholder=""
|
||||
spellCheck={false}
|
||||
|
||||
@@ -72,7 +72,7 @@ it("change custom css", async () => {
|
||||
let settingsRecord = cloneDeep(settings);
|
||||
const updateSettingsStub = createSinonStub(s =>
|
||||
s.onFirstCall().callsFake((_: any, data: any) => {
|
||||
expect(data.input.settings.customCssUrl).toEqual("./custom.css");
|
||||
expect(data.input.settings.customCSSURL).toEqual("./custom.css");
|
||||
settingsRecord = merge(settingsRecord, data.input.settings);
|
||||
return {
|
||||
settings: settingsRecord,
|
||||
|
||||
@@ -17,7 +17,7 @@ export const settings = {
|
||||
closedTimeout: 604800,
|
||||
autoCloseStream: false,
|
||||
closedMessage: null,
|
||||
customCssUrl: null,
|
||||
customCSSURL: null,
|
||||
domains: ["localhost:8080"],
|
||||
editCommentWindowLength: 30000,
|
||||
communityGuidelines: {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user