diff --git a/config/async-component-loader.js b/config/async-component-loader.js index 10131ddb..90ca4cd3 100644 --- a/config/async-component-loader.js +++ b/config/async-component-loader.js @@ -3,27 +3,27 @@ let componentPath = require.resolve('./async-component'); module.exports = function () { }; module.exports.pitch = function (remainingRequest) { - this.cacheable && this.cacheable(); - let query = loaderUtils.getOptions(this) || {}; - let routeName = typeof query.name === 'function' ? query.name(this.resourcePath) : null; - let name; - if (routeName !== null) { - name = routeName; - } - else if ('name' in query) { - name = query.name; - } - else if ('formatName' in query) { - name = query.formatName(this.resourcePath); - } + this.cacheable && this.cacheable(); + let query = loaderUtils.getOptions(this) || {}; + let routeName = typeof query.name === 'function' ? query.name(this.resourcePath) : null; + let name; + if (routeName !== null) { + name = routeName; + } + else if ('name' in query) { + name = query.name; + } + else if ('formatName' in query) { + name = query.formatName(this.resourcePath); + } - return ` - import async from ${JSON.stringify(componentPath)}; - function load(cb) { - require.ensure([], function (require) { - cb( require(${loaderUtils.stringifyRequest(this, '!!' + remainingRequest)}) ); - }${name ? (', ' + JSON.stringify(name)) : ''}); - } - export default async(load); - `; + return ` + import async from ${JSON.stringify(componentPath)}; + function load(cb) { + require.ensure([], function (require) { + cb( require(${loaderUtils.stringifyRequest(this, '!!' + remainingRequest)}) ); + }${name ? (', ' + JSON.stringify(name)) : ''}); + } + export default async(load); + `; }; \ No newline at end of file diff --git a/config/async-component.js b/config/async-component.js index b9ba69f3..b4dfc4d4 100644 --- a/config/async-component.js +++ b/config/async-component.js @@ -1,30 +1,30 @@ import { h, Component } from 'preact'; export default function (req) { - function Async() { - Component.call(this); + function Async() { + Component.call(this); - let b, old; - this.componentWillMount = () => { - b = this.base = this.nextBase || this.__b; // short circuits 1st render - req(m => { - this.setState({ child: m.default || m }); - }); - }; + let b, old; + this.componentWillMount = () => { + b = this.base = this.nextBase || this.__b; // short circuits 1st render + req(m => { + this.setState({ child: m.default || m }); + }); + }; - this.shouldComponentUpdate = (_, nxt) => { - nxt = nxt.child === void 0; - if (nxt && old === void 0 && !!b) { - old = h(b.nodeName, { dangerouslySetInnerHTML: { __html: b.innerHTML } }); - } - else { - old = ''; // dump it - } - return !nxt; - }; + this.shouldComponentUpdate = (_, nxt) => { + nxt = nxt.child === void 0; + if (nxt && old === void 0 && !!b) { + old = h(b.nodeName, { dangerouslySetInnerHTML: { __html: b.innerHTML } }); + } + else { + old = ''; // dump it + } + return !nxt; + }; - this.render = (p, s) => s.child ? h(s.child, p) : old; - } - (Async.prototype = new Component()).constructor = Async; - return Async; + this.render = (p, s) => s.child ? h(s.child, p) : old; + } + (Async.prototype = new Component()).constructor = Async; + return Async; } \ No newline at end of file diff --git a/config/client-boot.js b/config/client-boot.js deleted file mode 100644 index 742acf1b..00000000 --- a/config/client-boot.js +++ /dev/null @@ -1,27 +0,0 @@ -import { h, render } from 'preact'; - -if (process.env.NODE_ENV === 'development') { - // enable preact devtools - require('preact/debug'); -} -else if (process.env.ADD_SW && 'serviceWorker' in navigator && location.protocol === 'https:') { - // eslint-disable-next-line no-undef - navigator.serviceWorker.register(__webpack_public_path__ + 'sw.js'); -} - -const interopDefault = m => m && m.default ? m.default : m; - -let app = interopDefault(require('app-entry-point')); - -if (typeof app === 'function') { - let root = document.getElementById('app') || document.body.firstElementChild; - - let init = () => { - let app = interopDefault(require('app-entry-point')); - root = render(h(app), document.body, root); - }; - - if (module.hot) module.hot.accept('app-entry-point', init); - - init(); -} diff --git a/config/prerender-loader.js b/config/prerender-loader.js new file mode 100644 index 00000000..91f0647a --- /dev/null +++ b/config/prerender-loader.js @@ -0,0 +1,64 @@ +const path = require('path'); +const vm = require('vm'); + +module.exports = function (content) { + const jsdom = require('jsdom'); + const preact = require('preact'); + const renderToString = require('preact-render-to-string'); + + this.cacheable && this.cacheable(); + + const callback = this.async(); + + // const dom = new jsdom.JSDOM(``, { + const dom = new jsdom.JSDOM(content, { + includeNodeLocations: false, + runScripts: 'outside-only' + }); + const { window } = dom; + const { document } = window; + + // console.log(content); + + const root = document.getElementById('app'); + this.loadModule(path.join(__dirname, 'client-boot.js'), (err, source) => { + if (err) return callback(err); + + console.log(source); + + let mod = eval(source); + let props = {}; + // console.log(mod); + let vnode = preact.createElement(mod, props); + let frag = document.createElement('div'); + frag.innerHTML = renderToString(vnode); + root.parentNode.replaceChild(frag.firstChild, root); + + let html = dom.serialize(); + callback(null, html); + // return html = `module.exports = ${JSON.stringify(html)}`; + // return 'module.exports = ' + JSON.stringify(content).replace(/\{\{PRERENDER\}\}/gi, `" + require("preact-render-to-string")(require("app-entry-point")) + "`); + }); + + // global.window = global; + // global.document = {}; + // return 'module.exports = ' + JSON.stringify(content).replace(/\{\{PRERENDER\}\}/gi, `" + require("preact-render-to-string")(require("app-entry-point")) + "`); + + /* + let callback = this.async(); + + let parts = content.split(/\{\{prerender\}\}/gi); + + if (parts.length<2) { + // callback(null, `module.exports = ${JSON.stringify(content)}`); + callback(null, content); + return; + } + + // let html = ` + // window = {}; + // module.exports = ${JSON.stringify(parts[0])} + require("preact-render-to-string")(require("app-entry-point")) + ${JSON.stringify(parts[1])}`; + let html = `module.exports = ${JSON.stringify(parts[0])} + require("preact-render-to-string")(require("app-entry-point")) + ${JSON.stringify(parts[1])}`; + callback(null, html); + */ +}; \ No newline at end of file diff --git a/config/prerender.js b/config/prerender.js index 14f7f83c..65fda441 100644 --- a/config/prerender.js +++ b/config/prerender.js @@ -5,16 +5,16 @@ let renderToString = require('preact-render-to-string'); let appPath = path.join(__dirname, '../src/index'); module.exports = function(options) { - options = options || {}; - let url = typeof options==='string' ? options : options.url; - global.history = {}; - global.location = { href: url, pathname: url }; + options = options || {}; + let url = typeof options==='string' ? options : options.url; + global.history = {}; + global.location = { href: url, pathname: url }; - // let app = require('app-entry-point'); - let app = require(appPath); + // let app = require('app-entry-point'); + let app = require(appPath); - let html = renderToString(preact.h(app, { url })); - console.log(html); + let html = renderToString(preact.h(app, { url })); + console.log(html); - return html; + return html; }; diff --git a/config/watch-timestamps-plugin.js b/config/watch-timestamps-plugin.js index 6db23a03..942ddb85 100644 --- a/config/watch-timestamps-plugin.js +++ b/config/watch-timestamps-plugin.js @@ -8,23 +8,23 @@ const fs = require('fs'); * https://github.com/Jimdo/typings-for-css-modules-loader/issues/48#issuecomment-347036461 */ module.exports = class WatchTimestampsPlugin { - constructor(patterns) { - this.patterns = patterns; - } + constructor(patterns) { + this.patterns = patterns; + } - apply(compiler) { - compiler.plugin('watch-run', (watch, callback) => { - const patterns = this.patterns; - const timestamps = watch.fileTimestamps; + apply(compiler) { + compiler.plugin('watch-run', (watch, callback) => { + const patterns = this.patterns; + const timestamps = watch.fileTimestamps; - for (const filepath of timestamps) { - if (patterns.some(pat => pat instanceof RegExp ? pat.test(filepath) : filepath.indexOf(pat) === 0)) { - let time = fs.statSync(filepath).mtime; - if (timestamps instanceof Map) timestamps.set(filepath, time); - else timestamps[filepath] = time; - } - } - callback(); - }); - } + for (const filepath of timestamps) { + if (patterns.some(pat => pat instanceof RegExp ? pat.test(filepath) : filepath.indexOf(pat) === 0)) { + let time = fs.statSync(filepath).mtime; + if (timestamps instanceof Map) timestamps.set(filepath, time); + else timestamps[filepath] = time; + } + } + callback(); + }); + } }; diff --git a/src/assets/icon.png b/src/assets/icon.png index fb9b1fc4..8371feaa 100644 Binary files a/src/assets/icon.png and b/src/assets/icon.png differ diff --git a/src/components/app/style.scss b/src/components/app/style.scss index dd1192d6..e3c6e10b 100644 --- a/src/components/app/style.scss +++ b/src/components/app/style.scss @@ -1,21 +1,25 @@ @import '~style/helpers.scss'; .app { - position: absolute; - top: $toolbar-height; - left: 0; - width: 100%; - bottom: 0; - contain: size layout style; - overflow: auto; - -webkit-overflow-scrolling: touch; + position: absolute; + display: flex; + flex-direction: column; + top: 0; + left: 0; + width: 100%; + bottom: 0; + overflow: hidden; + z-index: 1; - > .content { - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - overflow: visible; - } + .header { + flex: 0 0 auto; + position: relative; + } + + .content { + flex: 1 1 auto; + contain: size layout style; + overflow: auto; + -webkit-overflow-scrolling: touch; + } } diff --git a/src/components/app/style.scss.d.ts b/src/components/app/style.scss.d.ts index 9a895562..44bf86f2 100644 --- a/src/components/app/style.scss.d.ts +++ b/src/components/app/style.scss.d.ts @@ -1,2 +1,3 @@ export const app: string; +export const header: string; export const content: string; diff --git a/src/components/drawer/style.scss b/src/components/drawer/style.scss index e5751318..41acac3e 100644 --- a/src/components/drawer/style.scss +++ b/src/components/drawer/style.scss @@ -1,29 +1,29 @@ @import '~style/helpers.scss'; :global { - // @import '~preact-material-components/Drawer/style.css'; - @import '~preact-material-components/List/mdc-list.scss'; + // @import '~preact-material-components/Drawer/style.css'; + @import '~preact-material-components/List/mdc-list.scss'; } .drawer { - :global(.mdc-list-item__start-detail) { - margin-right: 16px; - } + :global(.mdc-list-item__start-detail) { + margin-right: 16px; + } } .logo { - width: 50%; + width: 50%; } .category img { - opacity: .6; + opacity: .6; } .bottom { - position: absolute; - bottom: 0; - bottom: constant(safe-area-inset-bottom); - bottom: env(safe-area-inset-bottom); - left: 0; - width: 100%; + position: absolute; + bottom: 0; + bottom: constant(safe-area-inset-bottom); + bottom: env(safe-area-inset-bottom); + left: 0; + width: 100%; } diff --git a/src/components/fab/style.scss b/src/components/fab/style.scss index 765f50b2..69e67345 100644 --- a/src/components/fab/style.scss +++ b/src/components/fab/style.scss @@ -1,18 +1,18 @@ @import '~style/helpers.scss'; :global { - @import '~preact-material-components/Fab/mdc-fab.scss'; + @import '~preact-material-components/Fab/mdc-fab.scss'; } .fab { - position: fixed; - right: 14px; - bottom: 14px; - z-index: 4; - - .progress { - width: 24px; - height: 24px; - color: white; - --mdc-theme-primary: #fff; - } + position: fixed; + right: 14px; + bottom: 14px; + z-index: 4; + + .progress { + width: 24px; + height: 24px; + color: white; + --mdc-theme-primary: #fff; + } } diff --git a/src/components/header/style.scss b/src/components/header/style.scss index 8c22f295..61bee8ce 100644 --- a/src/components/header/style.scss +++ b/src/components/header/style.scss @@ -1,51 +1,52 @@ @import '~style/helpers.scss'; :global { - @import '~preact-material-components/Toolbar/mdc-toolbar.scss'; + @import '~preact-material-components/Toolbar/mdc-toolbar.scss'; } .toolbar { - height: $toolbar-height; + // height: $toolbar-height; - &.minimal { - height: $toolbar-height / 2; - } + &.minimal { + display: none; + // height: $toolbar-height / 2; + } - // > * { - // min-height: 0; - // } + // > * { + // min-height: 0; + // } } .fileInput { - position: absolute; - left: 0; - top: -999px; + position: absolute; + left: 0; + top: -999px; } .fab { - position: fixed; - display: block; - right: 14px; - bottom: 14px; - // z-index: 999; - // transform: translateZ(0); + position: fixed; + display: block; + right: 14px; + bottom: 14px; + // z-index: 999; + // transform: translateZ(0); } .logo { - height: 1em; + height: 1em; } - + .menu { - position: absolute; - top: $toolbar-height; - right: 5px; + position: absolute; + top: $toolbar-height; + right: 5px; - .menuItem { - margin-right: 16px; - } + .menuItem { + margin-right: 16px; + } } .title { - padding: 3px 0 0; - font-weight: 300; - font-size: 140%; + padding: 3px 0 0; + font-weight: 300; + font-size: 140%; } \ No newline at end of file diff --git a/src/components/home/style.scss b/src/components/home/style.scss index ee19608e..09d9fa5b 100644 --- a/src/components/home/style.scss +++ b/src/components/home/style.scss @@ -1,20 +1,20 @@ @import '~style/helpers.scss'; // :global { -// @import '~preact-material-components/Button/mdc-button.scss'; -// // @import '~preact-material-components/Switch/mdc-switch.scss'; +// @import '~preact-material-components/Button/mdc-button.scss'; +// // @import '~preact-material-components/Switch/mdc-switch.scss'; // } .home { - padding: 20px; - opacity: 0; + padding: 20px; + opacity: 0; } .active { - animation: fadeIn 2s forwards ease 1; + animation: fadeIn 2s forwards ease 1; } @keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } + from { opacity: 0; } + to { opacity: 1; } } \ No newline at end of file diff --git a/src/index.html b/src/index.html index 8f003453..6fecc701 100644 --- a/src/index.html +++ b/src/index.html @@ -6,23 +6,11 @@ + - - <%= - /*require('../config/prerender')()*/ - htmlWebpackPlugin.options.prerender() - %> +
+ \ No newline at end of file diff --git a/src/lib/fix-pmc.js b/src/lib/fix-pmc.js index 99eaf243..6dd10509 100644 --- a/src/lib/fix-pmc.js +++ b/src/lib/fix-pmc.js @@ -1,26 +1,26 @@ import { options } from 'preact'; const classNameDescriptor = { - enumerable: false, - configurable: true, - get() { - return this.class; - }, - set(value) { - this.class = value; - } + enumerable: false, + configurable: true, + get() { + return this.class; + }, + set(value) { + this.class = value; + } }; let old = options.vnode; options.vnode = vnode => { - let a = vnode.attributes; - if (a!=null) { - if ('className' in a) { - a.class = a.className; - } - if ('class' in a) { - Object.defineProperty(a, 'className', classNameDescriptor); - } - } - if (old != null) old(vnode); -}; \ No newline at end of file + let a = vnode.attributes; + if (a != null) { + if ('className' in a) { + a.class = a.className; + } + if ('class' in a) { + Object.defineProperty(a, 'className', classNameDescriptor); + } + } + if (old != null) old(vnode); +}; diff --git a/src/style/index.scss b/src/style/index.scss index 8143c1f9..2795b423 100644 --- a/src/style/index.scss +++ b/src/style/index.scss @@ -1,26 +1,27 @@ // @import './material-icons.scss'; // @import 'material-components-web/material-components-web'; @import './reset.scss'; +@import url('https://fonts.googleapis.com/icon?family=Material+Icons'); html, body { - height: 100%; - width: 100%; - padding: 0; - margin: 0; - overflow: hidden; - overscroll-behavior: none; + height: 100%; + width: 100%; + padding: 0; + margin: 0; + overflow: hidden; + overscroll-behavior: none; } html { - background: #FAFAFA; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", Helvetica, Arial, sans-serif; - font-weight: 400; - color: #444; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; + background: #FAFAFA; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 400; + color: #444; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } .mdc-theme--dark { - background-color: #333; - color: #fff; + background-color: #333; + color: #fff; } \ No newline at end of file diff --git a/src/style/material-icons.scss b/src/style/material-icons.scss index d0bb3be2..1ad3e638 100644 --- a/src/style/material-icons.scss +++ b/src/style/material-icons.scss @@ -1,28 +1,28 @@ @font-face { - font-family: 'Material Icons'; - font-style: normal; - font-weight: 400; - src: local('Material Icons'), - local('MaterialIcons-Regular'), - url(https://example.com/MaterialIcons-Regular.woff2) format('woff2'), - url(https://example.com/MaterialIcons-Regular.woff) format('woff'), - url(https://example.com/MaterialIcons-Regular.ttf) format('truetype'); + font-family: 'Material Icons'; + font-style: normal; + font-weight: 400; + src: local('Material Icons'), + local('MaterialIcons-Regular'), + url(https://example.com/MaterialIcons-Regular.woff2) format('woff2'), + url(https://example.com/MaterialIcons-Regular.woff) format('woff'), + url(https://example.com/MaterialIcons-Regular.ttf) format('truetype'); } .material-icons { - font-family: 'Material Icons'; - font-weight: normal; - font-style: normal; - font-size: 24px; - display: inline-block; - line-height: 1; - text-transform: none; - letter-spacing: normal; - word-wrap: normal; - white-space: nowrap; - direction: ltr; - -webkit-font-smoothing: antialiased; - text-rendering: optimizeLegibility; - -moz-osx-font-smoothing: grayscale; - font-feature-settings: 'liga'; + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 24px; + display: inline-block; + line-height: 1; + text-transform: none; + letter-spacing: normal; + word-wrap: normal; + white-space: nowrap; + direction: ltr; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + -moz-osx-font-smoothing: grayscale; + font-feature-settings: 'liga'; } \ No newline at end of file diff --git a/src/style/reset.scss b/src/style/reset.scss index 4e6eb882..c863fb75 100644 --- a/src/style/reset.scss +++ b/src/style/reset.scss @@ -1,12 +1,12 @@ button, a, img, input, select, textarea { - -webkit-tap-highlight-color: rgba(0,0,0,0); + -webkit-tap-highlight-color: rgba(0,0,0,0); } a, button, img, [inert], .inert { - user-select: none; - -webkit-user-select: none; - user-drag: none; - -webkit-user-drag: none; - touch-callout: none; - -webkit-touch-callout: none; + user-select: none; + -webkit-user-select: none; + user-drag: none; + -webkit-user-drag: none; + touch-callout: none; + -webkit-touch-callout: none; } diff --git a/webpack.config.js b/webpack.config.js index 4382f78d..6fc65dbb 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -10,114 +10,114 @@ const HtmlWebpackPlugin = require('html-webpack-plugin'); const PreloadWebpackPlugin = require('preload-webpack-plugin'); const ReplacePlugin = require('webpack-plugin-replace'); const CopyPlugin = require('copy-webpack-plugin'); +const WorkboxPlugin = require('workbox-webpack-plugin'); const WatchTimestampsPlugin = require('./config/watch-timestamps-plugin'); const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; function readJson(filename) { - return JSON.parse(fs.readFileSync(filename)); + return JSON.parse(fs.readFileSync(filename)); } module.exports = function(_, env) { - const isProd = env.mode === 'production'; - const nodeModules = path.join(__dirname, 'node_modules'); - const componentStyleDirs = [ - path.join(__dirname, 'src/components'), - path.join(__dirname, 'src/routes') - ]; + const isProd = env.mode === 'production'; + const nodeModules = path.join(__dirname, 'node_modules'); + const componentStyleDirs = [ + path.join(__dirname, 'src/components'), + path.join(__dirname, 'src/routes') + ]; - return { - mode: isProd ? 'production' : 'development', - entry: path.join(__dirname, 'config/client-boot.js'), - output: { - filename: isProd ? '[name].[chunkhash:5].js' : '[name].js', - chunkFilename: '[name].chunk.[chunkhash:5].js', - path: path.join(__dirname, 'build'), - publicPath: '/' - }, - resolve: { - extensions: ['.ts', '.tsx', '.js', '.jsx', '.scss', '.css'], - alias: { - 'app-entry-point': path.join(__dirname, 'src/index'), - style: path.join(__dirname, 'src/style') - } - }, - resolveLoader: { - alias: { - // async-component-loader returns a wrapper component that waits for the import to load before rendering: - async: path.join(__dirname, 'config/async-component-loader') - } - }, - module: { - rules: [ - { - test: /\.tsx?$/, - // Ensure typescript is compiled prior to Babel running: - enforce: 'pre', - loader: 'ts-loader', - // Don't transpile anything in node_modules: - exclude: nodeModules, + return { + mode: isProd ? 'production' : 'development', + entry: './src/index', + output: { + filename: isProd ? '[name].[chunkhash:5].js' : '[name].js', + chunkFilename: '[name].chunk.[chunkhash:5].js', + path: path.join(__dirname, 'build'), + publicPath: '/' + }, + resolve: { + extensions: ['.ts', '.tsx', '.js', '.jsx', '.scss', '.css'], + alias: { + style: path.join(__dirname, 'src/style') + } + }, + resolveLoader: { + alias: { + // async-component-loader returns a wrapper component that waits for the import to load before rendering: + async: path.join(__dirname, 'config/async-component-loader') + } + }, + module: { + rules: [ + { + test: /\.tsx?$/, + // Ensure typescript is compiled prior to Babel running: + enforce: 'pre', + loader: 'ts-loader', + // Don't transpile anything in node_modules: + exclude: nodeModules, options: { // Offload type checking to ForkTsCheckerWebpackPlugin for better performance: transpileOnly: true } - }, - { - test: /\.(tsx?|jsx?)$/, - loader: 'babel-loader', - // Don't respect any Babel RC files found on the filesystem: - options: Object.assign(readJson('.babelrc'), { babelrc: false }) - }, - { - test: /\.(scss|sass)$/, - loader: 'sass-loader', - // SCSS gets preprocessed, then treated like any other CSS: - enforce: 'pre', - options: { - sourceMap: true, - includePaths: [nodeModules] - } - }, - { - test: /\.(scss|sass|css)$/, - // Only enable CSS Modules within `src/{components,routes}/*` - include: componentStyleDirs, - use: [ - // In production, CSS is extracted to files on disk. In development, it's inlined into JS: - isProd ? MiniCssExtractPlugin.loader : 'style-loader', - { - // This is a fork of css-loader that auto-generates .d.ts files for CSS module imports. - // The result is a definition file with the exported String classname mappings. - loader: 'typings-for-css-modules-loader', - options: { - modules: true, - localIdentName: '[local]__[hash:base64:5]', - namedExport: true, - camelCase: true, - importLoaders: 1, - sourceMap: isProd, - sass: true - } - } - ] - }, - { - test: /\.(scss|sass|css)$/, - // Process non-modular CSS everywhere *except* `src/{components,routes}/*` - exclude: componentStyleDirs, - use: [ - isProd ? MiniCssExtractPlugin.loader : 'style-loader', - { - loader: 'css-loader', - options: { - importLoaders: 1, - sourceMap: isProd - } - } - ] - } - ] - }, - plugins: [ + }, + { + test: /\.(tsx?|jsx?)$/, + loader: 'babel-loader', + // Don't respect any Babel RC files found on the filesystem: + options: Object.assign(readJson('.babelrc'), { babelrc: false }) + }, + { + test: /\.(scss|sass)$/, + loader: 'sass-loader', + // SCSS gets preprocessed, then treated like any other CSS: + enforce: 'pre', + options: { + sourceMap: true, + includePaths: [nodeModules] + } + }, + { + test: /\.(scss|sass|css)$/, + // Only enable CSS Modules within `src/{components,routes}/*` + include: componentStyleDirs, + use: [ + // In production, CSS is extracted to files on disk. In development, it's inlined into JS: + isProd ? MiniCssExtractPlugin.loader : 'style-loader', + { + // This is a fork of css-loader that auto-generates .d.ts files for CSS module imports. + // The result is a definition file with the exported String classname mappings. + loader: 'typings-for-css-modules-loader', + options: { + modules: true, + localIdentName: '[local]__[hash:base64:5]', + namedExport: true, + camelCase: true, + importLoaders: 1, + sourceMap: isProd, + sass: true + } + } + ] + }, + { + test: /\.(scss|sass|css)$/, + // Process non-modular CSS everywhere *except* `src/{components,routes}/*` + exclude: componentStyleDirs, + use: [ + isProd ? MiniCssExtractPlugin.loader : 'style-loader', + { + loader: 'css-loader', + options: { + importLoaders: 1, + sourceMap: isProd + } + } + ] + } + ] + }, + plugins: [ // Runs tslint & type checking in a worker pool new ForkTsCheckerWebpackPlugin({ tslint: true, @@ -127,140 +127,140 @@ module.exports = function(_, env) { }), new ForkTsCheckerNotifierWebpackPlugin({ excludeWarnings: true }), - // Pretty progressbar showing build progress: - new ProgressBarPlugin({ - format: '\u001b[90m\u001b[44mBuild\u001b[49m\u001b[39m [:bar] \u001b[32m\u001b[1m:percent\u001b[22m\u001b[39m (:elapseds) \u001b[2m:msg\u001b[22m', - renderThrottle: 100, - summary: false, - clear: true - }), + // Pretty progressbar showing build progress: + new ProgressBarPlugin({ + format: '\u001b[90m\u001b[44mBuild\u001b[49m\u001b[39m [:bar] \u001b[32m\u001b[1m:percent\u001b[22m\u001b[39m (:elapseds) \u001b[2m:msg\u001b[22m', + renderThrottle: 100, + summary: false, + clear: true + }), - // Remove old files before outputting a production build: - isProd && new CleanWebpackPlugin([ - 'assets', - '**/*.{css,js,json,html}' - ], { - root: path.join(__dirname, 'build'), - beforeEmit: true - }), + // Remove old files before outputting a production build: + isProd && new CleanWebpackPlugin([ + 'assets', + '**/*.{css,js,json,html}' + ], { + root: path.join(__dirname, 'build'), + beforeEmit: true + }), - // Automatically split code into async chunks. - // See: https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366 - isProd && new webpack.optimize.SplitChunksPlugin({}), + // Automatically split code into async chunks. + // See: https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366 + isProd && new webpack.optimize.SplitChunksPlugin({}), - // In production, extract all CSS to produce files on disk, even for - // lazy-loaded CSS chunks. CSS for async chunks is loaded on-demand. - // This is a modern Webpack 4 replacement for ExtractTextPlugin. - // See: https://github.com/webpack-contrib/mini-css-extract-plugin - // See also: https://twitter.com/wsokra/status/970253245733113856 - isProd && new MiniCssExtractPlugin({ - chunkFilename: '[name].chunk.[contenthash:5].css' - }), + // In production, extract all CSS to produce files on disk, even for + // lazy-loaded CSS chunks. CSS for async chunks is loaded on-demand. + // This is a modern Webpack 4 replacement for ExtractTextPlugin. + // See: https://github.com/webpack-contrib/mini-css-extract-plugin + // See also: https://twitter.com/wsokra/status/970253245733113856 + isProd && new MiniCssExtractPlugin({ + chunkFilename: '[name].chunk.[contenthash:5].css' + }), - // These plugins fix infinite loop in typings-for-css-modules-loader. - // See: https://github.com/Jimdo/typings-for-css-modules-loader/issues/35 - new webpack.WatchIgnorePlugin([ - /(c|sc|sa)ss\.d\.ts$/ - ]), - new WatchTimestampsPlugin([ - /(c|sc|sa)ss\.d\.ts$/ - ]), + // These plugins fix infinite loop in typings-for-css-modules-loader. + // See: https://github.com/Jimdo/typings-for-css-modules-loader/issues/35 + new webpack.WatchIgnorePlugin([ + /(c|sc|sa)ss\.d\.ts$/ + ]), + new WatchTimestampsPlugin([ + /(c|sc|sa)ss\.d\.ts$/ + ]), - // For now we're not doing SSR. - new HtmlWebpackPlugin({ - filename: path.join(__dirname, 'build/index.html'), - template: '!!ejs-loader!src/index.html', - // template: '!!'+path.join(__dirname, 'config/prerender-loader')+'!src/index.html', - minify: isProd && { - collapseWhitespace: true, - removeScriptTypeAttributes: true, - removeRedundantAttributes: true, - removeStyleLinkTypeAttributes: true, - removeComments: true - }, - manifest: readJson('./src/manifest.json'), - inject: true, - compile: true - }), + // For now we're not doing SSR. + new HtmlWebpackPlugin({ + filename: path.join(__dirname, 'build/index.html'), + template: '!!ejs-loader!src/index.html', + // template: '!!'+path.join(__dirname, 'config/prerender-loader')+'!src/index.html', + minify: isProd && { + collapseWhitespace: true, + removeScriptTypeAttributes: true, + removeRedundantAttributes: true, + removeStyleLinkTypeAttributes: true, + removeComments: true + }, + manifest: readJson('./src/manifest.json'), + inject: true, + compile: true + }), - // Inject for resources - isProd && new PreloadWebpackPlugin(), + // Inject for resources + isProd && new PreloadWebpackPlugin(), - // Inline constants during build, so they can be folded by UglifyJS. - new webpack.DefinePlugin({ - // We set node.process=false later in this config. - // Here we make sure if (process && process.foo) still works: - process: '{}' - }), + // Inline constants during build, so they can be folded by UglifyJS. + new webpack.DefinePlugin({ + // We set node.process=false later in this config. + // Here we make sure if (process && process.foo) still works: + process: '{}' + }), - // Babel embeds helpful error messages into transpiled classes that we don't need in production. - // Here we replace the constructor and message with a static throw, leaving the message to be DCE'd. - // This is useful since it shows the message in SourceMapped code when debugging. - isProd && new ReplacePlugin({ - include: /babel-helper$/, - patterns: [{ - regex: /throw\s+(?:new\s+)?((?:Type|Reference)?Error)\s*\(/g, - value: (s, type) => `throw 'babel error'; (` - }] - }), + // Babel embeds helpful error messages into transpiled classes that we don't need in production. + // Here we replace the constructor and message with a static throw, leaving the message to be DCE'd. + // This is useful since it shows the message in SourceMapped code when debugging. + isProd && new ReplacePlugin({ + include: /babel-helper$/, + patterns: [{ + regex: /throw\s+(?:new\s+)?((?:Type|Reference)?Error)\s*\(/g, + value: (s, type) => `throw 'babel error'; (` + }] + }), - // Copying files via Webpack allows them to be served dynamically by `webpack serve` - new CopyPlugin([ - { from: 'src/manifest.json', to: 'manifest.json' }, - { from: 'src/assets', to: 'assets' } - ]), + // Copying files via Webpack allows them to be served dynamically by `webpack serve` + new CopyPlugin([ + { from: 'src/manifest.json', to: 'manifest.json' }, + { from: 'src/assets', to: 'assets' } + ]), - // For production builds, output module size analysis to build/report.html - isProd && new BundleAnalyzerPlugin({ - analyzerMode: 'static', - defaultSizes: 'gzip', - openAnalyzer: false - }), + // For production builds, output module size analysis to build/report.html + isProd && new BundleAnalyzerPlugin({ + analyzerMode: 'static', + defaultSizes: 'gzip', + openAnalyzer: false + }), - // Generate a ServiceWorker using Workbox. - isProd && new WorkboxPlugin.GenerateSW({ - swDest: 'sw.js', - clientsClaim: true, - skipWaiting: true, - // allow for offline client-side routing: - navigateFallback: '/', - navigateFallbackBlacklist: [/\.[a-z0-9]+$/i] - }) - ].filter(Boolean), // Filter out any falsey plugin array entries. + // Generate a ServiceWorker using Workbox. + isProd && new WorkboxPlugin.GenerateSW({ + swDest: 'sw.js', + clientsClaim: true, + skipWaiting: true, + // allow for offline client-side routing: + navigateFallback: '/', + navigateFallbackBlacklist: [/\.[a-z0-9]+$/i] + }) + ].filter(Boolean), // Filter out any falsey plugin array entries. - // Turn off various NodeJS environment polyfills Webpack adds to bundles. - // They're supposed to be added only when used, but the heuristic is loose - // (eg: existence of a variable called setImmedaite in any scope) - node: { - console: false, - // Keep global, it's just an alias of window and used by many third party modules: - global: true, - // Turn off process to avoid bundling a nextTick implementation: - process: false, - // Inline __filename and __dirname values: - __filename: 'mock', - __dirname: 'mock', - // Never embed a portable implementation of Node's Buffer module: - Buffer: false, - // Never embed a setImmediate implementation: - setImmediate: false - }, + // Turn off various NodeJS environment polyfills Webpack adds to bundles. + // They're supposed to be added only when used, but the heuristic is loose + // (eg: existence of a variable called setImmedaite in any scope) + node: { + console: false, + // Keep global, it's just an alias of window and used by many third party modules: + global: true, + // Turn off process to avoid bundling a nextTick implementation: + process: false, + // Inline __filename and __dirname values: + __filename: 'mock', + __dirname: 'mock', + // Never embed a portable implementation of Node's Buffer module: + Buffer: false, + // Never embed a setImmediate implementation: + setImmediate: false + }, - devServer: { - // Any unmatched request paths will serve static files from src/*: - contentBase: path.join(__dirname, 'src'), - inline: true, - hot: true, - // Request paths not ending in a file extension serve index.html: - historyApiFallback: true, - // Don't output server address info to console on startup: - noInfo: true, - // Suppress forwarding of Webpack logs to the browser console: - clientLogLevel: 'none', - // Supress the extensive stats normally printed after a dev build (since sizes are mostly useless): - stats: 'minimal', - // Don't embed an error overlay ("redbox") into the client bundle: - overlay: false - } - }; + devServer: { + // Any unmatched request paths will serve static files from src/*: + contentBase: path.join(__dirname, 'src'), + inline: true, + hot: true, + // Request paths not ending in a file extension serve index.html: + historyApiFallback: true, + // Don't output server address info to console on startup: + noInfo: true, + // Suppress forwarding of Webpack logs to the browser console: + clientLogLevel: 'none', + // Supress the extensive stats normally printed after a dev build (since sizes are mostly useless): + stats: 'minimal', + // Don't embed an error overlay ("redbox") into the client bundle: + overlay: false + } + }; };