diff --git a/package-lock.json b/package-lock.json index 58f41bac9..ba29b526d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index ac6cc461e..6cbc7f219 100644 --- a/package.json +++ b/package.json @@ -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" ] diff --git a/src/core/build/createWebpackConfig.ts b/src/core/build/createWebpackConfig.ts index 7553ad06c..feddb4456 100644 --- a/src/core/build/createWebpackConfig.ts +++ b/src/core/build/createWebpackConfig.ts @@ -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 - ? (...nodes: T[]) => nodes - : (...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 +{% 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 %} +
+{% 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 %} diff --git a/src/core/server/app/views/macros.html b/src/core/server/app/views/macros.html new file mode 100644 index 000000000..37ff65d7f --- /dev/null +++ b/src/core/server/app/views/macros.html @@ -0,0 +1,19 @@ +{% macro css(src, integrity = '', prefix = '') %} + + {# TODO: evaluate when to enable SRI, non-SSL connections cause issues #} + {# {% if integrity %} + + {% else %} + + {% endif %} #} +{% endmacro %} + +{% macro js(src, integrity = '', prefix = '') %} + + {# TODO: evaluate when to enable SRI, non-SSL connections cause issues #} + {# {% if false %} + + {% else %} + + {% endif %} #} +{% endmacro %} diff --git a/src/core/server/app/views/partials/customCSS.html b/src/core/server/app/views/partials/customCSS.html new file mode 100644 index 000000000..d56d77add --- /dev/null +++ b/src/core/server/app/views/partials/customCSS.html @@ -0,0 +1,5 @@ +{% import "../macros.html" as macros %} + +{% if tenant and tenant.customCSSURL %} + {{ macros.css(tenant.customCSSURL) }} +{% endif %} diff --git a/src/core/server/app/views/templates/base.html b/src/core/server/app/views/templates/base.html new file mode 100644 index 000000000..b7266dedd --- /dev/null +++ b/src/core/server/app/views/templates/base.html @@ -0,0 +1,23 @@ + + + + {# Meta tags #} + + + {% block meta %}{% endblock %} + + {# Title #} + {% block title %}{% endblock %} + + {# CSS #} + {% block css %}{% endblock %} + + + {% block body %} + {% block html %} +
+ {% endblock %} + {% endblock %} + {% block js %}{% endblock %} + + diff --git a/src/core/server/graph/tenant/schema/schema.graphql b/src/core/server/graph/tenant/schema/schema.graphql index 5de5f6fa0..b3e645e2f 100644 --- a/src/core/server/graph/tenant/schema/schema.graphql +++ b/src/core/server/graph/tenant/schema/schema.graphql @@ -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 diff --git a/src/core/server/models/settings.ts b/src/core/server/models/settings.ts index 8609ea490..8b9215971 100644 --- a/src/core/server/models/settings.ts +++ b/src/core/server/models/settings.ts @@ -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