[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
+81 -49
View File
@@ -2101,10 +2101,13 @@
"dev": true
},
"@types/clean-css": {
"version": "3.4.30",
"resolved": "https://registry.npmjs.org/@types/clean-css/-/clean-css-3.4.30.tgz",
"integrity": "sha1-AFLBNvUkgAJCjjY4s33ko5gYZB0=",
"dev": true
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@types/clean-css/-/clean-css-4.2.0.tgz",
"integrity": "sha512-P+gDCIBAXZ/Q5e9d/Z9Rtn16P5Pr1YIO3gZcY7ZvaQ9ErgmOYtFQlLHZ2P/xcrIyN8TEZrI03EZmdmv1154sjA==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/commander": {
"version": "2.12.2",
@@ -2193,12 +2196,6 @@
"@types/enzyme": "*"
}
},
"@types/escape-string-regexp": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/escape-string-regexp/-/escape-string-regexp-1.0.0.tgz",
"integrity": "sha512-KAruqgk9/340M4MYYasdBET+lyYN8KMXUuRKWO72f4SbmIMMFp9nnJiXUkJS0HC2SFe4x0R/fLozXIzqoUfSjA==",
"dev": true
},
"@types/eventemitter2": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@types/eventemitter2/-/eventemitter2-4.1.0.tgz",
@@ -2746,6 +2743,16 @@
}
}
},
"@types/webpack-assets-manifest": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/webpack-assets-manifest/-/webpack-assets-manifest-3.0.0.tgz",
"integrity": "sha512-KDwIPcC3uwTownU5pIIm1BiWXvDKnqnv0HisAw3z3eiI/cFAJGi1ryUGnOQwy22lXDPsPmMkK4Os/PtF2LjGrQ==",
"dev": true,
"requires": {
"@types/tapable": "*",
"@types/webpack": "*"
}
},
"@types/webpack-bundle-analyzer": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/@types/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.13.0.tgz",
@@ -2767,15 +2774,6 @@
"@types/webpack": "*"
}
},
"@types/webpack-manifest-plugin": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/webpack-manifest-plugin/-/webpack-manifest-plugin-1.3.2.tgz",
"integrity": "sha512-ythLsrDoSLkEOrmKF22MbrweS4hHdMaM1C/2Fp1OOh7jPawy8ah4ajJPrLeEim8uAfZVe/V/jTEDc7VtSogU/w==",
"dev": true,
"requires": {
"@types/webpack": "*"
}
},
"@types/ws": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-5.1.2.tgz",
@@ -6399,7 +6397,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz",
"integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=",
"dev": true,
"requires": {
"no-case": "^2.2.0",
"upper-case": "^1.1.1"
@@ -6714,12 +6711,18 @@
"dev": true
},
"clean-css": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.1.11.tgz",
"integrity": "sha1-Ls3xRaujj1R0DybO/Q/z4D4SXWo=",
"dev": true,
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz",
"integrity": "sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==",
"requires": {
"source-map": "0.5.x"
"source-map": "~0.6.0"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
}
}
},
"cli-boxes": {
@@ -13213,25 +13216,28 @@
"dev": true
},
"html-minifier": {
"version": "3.5.17",
"resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.17.tgz",
"integrity": "sha512-O+StuKL0UWfwX5Zv4rFxd60DPcT5DVjGq1AlnP6VQ8wzudft/W4hx5Wl98aSYNwFBHY6XWJreRw/BehX4l+diQ==",
"dev": true,
"version": "3.5.21",
"resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.21.tgz",
"integrity": "sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==",
"requires": {
"camel-case": "3.0.x",
"clean-css": "4.1.x",
"commander": "2.15.x",
"he": "1.1.x",
"clean-css": "4.2.x",
"commander": "2.17.x",
"he": "1.2.x",
"param-case": "2.1.x",
"relateurl": "0.2.x",
"uglify-js": "3.4.x"
},
"dependencies": {
"commander": {
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
"integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==",
"dev": true
"version": "2.17.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
"integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg=="
},
"he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
}
}
},
@@ -17040,6 +17046,12 @@
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
"dev": true
},
"lodash.has": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz",
"integrity": "sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=",
"dev": true
},
"lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
@@ -17265,8 +17277,7 @@
"lower-case": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz",
"integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=",
"dev": true
"integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw="
},
"lowercase-keys": {
"version": "1.0.1",
@@ -18138,7 +18149,6 @@
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz",
"integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==",
"dev": true,
"requires": {
"lower-case": "^1.1.1"
}
@@ -18932,7 +18942,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz",
"integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=",
"dev": true,
"requires": {
"no-case": "^2.2.0"
}
@@ -23644,8 +23653,7 @@
"relateurl": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
"integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=",
"dev": true
"integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk="
},
"relay-compiler": {
"version": "1.7.0-rc.1",
@@ -27095,7 +27103,6 @@
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.2.tgz",
"integrity": "sha512-/kVQDzwiE9Vy7Y63eMkMozF4jIt0C2+xHctF9YpqNWdE/NLOuMurshkpoYGUlAbeYhACPv0HJPIHJul0Ak4/uw==",
"dev": true,
"requires": {
"commander": "~2.15.0",
"source-map": "~0.6.1"
@@ -27104,14 +27111,12 @@
"commander": {
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
"integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==",
"dev": true
"integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag=="
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
}
}
},
@@ -27512,8 +27517,7 @@
"upper-case": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz",
"integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=",
"dev": true
"integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg="
},
"uri-js": {
"version": "4.2.2",
@@ -27968,6 +27972,34 @@
}
}
},
"webpack-assets-manifest": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/webpack-assets-manifest/-/webpack-assets-manifest-3.1.1.tgz",
"integrity": "sha512-JV9V2QKc5wEWQptdIjvXDUL1ucbPLH2f27toAY3SNdGZp+xSaStAgpoMcvMZmqtFrBc9a5pTS1058vxyMPOzRQ==",
"dev": true,
"requires": {
"chalk": "^2.0",
"lodash.get": "^4.0",
"lodash.has": "^4.0",
"mkdirp": "^0.5",
"schema-utils": "^1.0.0",
"tapable": "^1.0.0",
"webpack-sources": "^1.0.0"
},
"dependencies": {
"schema-utils": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
"integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==",
"dev": true,
"requires": {
"ajv": "^6.1.0",
"ajv-errors": "^1.0.0",
"ajv-keywords": "^3.1.0"
}
}
}
},
"webpack-bundle-analyzer": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.0.3.tgz",
+6 -5
View File
@@ -75,11 +75,12 @@
"graphql-playground-html": "^1.6.0",
"graphql-redis-subscriptions": "^1.5.0",
"graphql-tools": "^3.0.5",
"html-minifier": "^3.5.21",
"html-to-text": "^4.0.0",
"ioredis": "^3.2.2",
"joi": "^13.4.0",
"jsonwebtoken": "^8.3.0",
"jsdom": "^11.12.0",
"jsonwebtoken": "^8.3.0",
"jwks-rsa": "^1.3.0",
"linkify-it": "^2.1.0",
"linkifyjs": "^2.1.7",
@@ -139,11 +140,11 @@
"@types/dotenv": "^4.0.3",
"@types/enzyme": "^3.1.15",
"@types/enzyme-adapter-react-16": "^1.0.3",
"@types/escape-string-regexp": "^1.0.0",
"@types/eventemitter2": "^4.1.0",
"@types/express": "^4.16.0",
"@types/fs-extra": "^5.0.4",
"@types/graphql": "^0.13.3",
"@types/html-minifier": "^3.5.2",
"@types/html-to-text": "^1.4.31",
"@types/html-webpack-plugin": "^3.2.0",
"@types/ioredis": "^3.2.12",
@@ -189,9 +190,9 @@
"@types/verror": "^1.10.3",
"@types/vinyl": "^2.0.2",
"@types/webpack": "^4.4.7",
"@types/webpack-assets-manifest": "^3.0.0",
"@types/webpack-bundle-analyzer": "^2.13.0",
"@types/webpack-dev-server": "^2.9.5",
"@types/webpack-manifest-plugin": "^1.3.2",
"@types/ws": "^5.1.2",
"autoprefixer": "^8.6.5",
"babel-core": "^7.0.0-bridge.0",
@@ -301,11 +302,11 @@
"typescript-snapshots-plugin": "^1.2.0",
"wait-for-expect": "^1.1.0",
"webpack": "^4.27.1",
"webpack-assets-manifest": "^3.1.1",
"webpack-bundle-analyzer": "^3.0.3",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.14",
"webpack-hot-client": "^4.1.1",
"webpack-manifest-plugin": "^2.0.4",
"whatwg-fetch": "^2.0.4"
},
"husky": {
@@ -314,7 +315,7 @@
}
},
"lint-staged": {
"*.{js,ts,tsx}": [
"*.{j,t}s{,x}": [
"tslint --fix",
"git add"
]
+79 -113
View File
@@ -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,
}),
...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 }}"
)
...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(
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);
}
);
});
};
}
@@ -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
@@ -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,
+1 -1
View File
@@ -17,7 +17,7 @@ export const settings = {
closedTimeout: 604800,
autoCloseStream: false,
closedMessage: null,
customCssUrl: null,
customCSSURL: null,
domains: ["localhost:8080"],
editCommentWindowLength: 30000,
communityGuidelines: {
+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