diff --git a/package.json b/package.json index 5bca1b8cf..dca22a743 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "devDependencies": { "babel-cli": "^6.24.0", "babel-eslint": "^7.2.1", + "babel-plugin-transform-runtime": "^6.23.0", "chrome-launcher": "^0.1.1", "chrome-remote-interface": "^0.23.2", "eslint": "^3.19.0", @@ -81,6 +82,8 @@ "html-webpack-exclude-assets-plugin": "0.0.5", "lodash": "^4.17.4", "ncp": "^2.0.0", + "node-sass": "^4.5.3", + "sass-loader": "^6.0.6", "tap-diff": "^0.1.1", "tape": "^4.6.3", "uuid": "^3.0.1" @@ -134,7 +137,7 @@ "rimraf": "^2.6.1", "script-ext-html-webpack-plugin": "^1.8.0", "simplehttp2server": "^2.0.0", - "sw-precache-webpack-plugin": "^0.11.0", + "sw-precache-webpack-plugin": "^0.11.2", "tmp": "0.0.31", "unfetch": "^3.0.0", "url-loader": "^0.5.8", diff --git a/src/commands/build.js b/src/commands/build.js index 48de6ee5e..1bab398cc 100644 --- a/src/commands/build.js +++ b/src/commands/build.js @@ -2,9 +2,7 @@ import { resolve } from 'path'; import promisify from 'es6-promisify'; import rimraf from 'rimraf'; import asyncCommand from '../lib/async-command'; -import webpackConfig from '../lib/webpack-config'; -import transformConfig from '../lib/transform-config'; -import runWebpack, { showStats, writeJsonStats } from '../lib/run-webpack'; +import runWebpack, { showStats, writeJsonStats } from '../lib/webpack/run-webpack'; export default asyncCommand({ command: 'build [src] [dest]', @@ -47,15 +45,12 @@ export default asyncCommand({ }, async handler(argv) { - let config = webpackConfig(argv); - await transformConfig(argv, config); - if (argv.clean) { let dest = resolve(argv.cwd || process.cwd(), argv.dest || 'build'); await promisify(rimraf)(dest); } - let stats = await runWebpack(false, config); + let stats = await runWebpack(false, argv); showStats(stats); if (argv.json) { diff --git a/src/commands/watch.js b/src/commands/watch.js index 29aa97741..b4c68482d 100644 --- a/src/commands/watch.js +++ b/src/commands/watch.js @@ -1,8 +1,6 @@ import asyncCommand from '../lib/async-command'; -import webpackConfig from '../lib/webpack-config'; -import transformConfig from '../lib/transform-config'; import getSslCert from '../lib/ssl-cert'; -import runWebpack, { showStats } from '../lib/run-webpack'; +import runWebpack, { showStats } from '../lib/webpack/run-webpack'; export default asyncCommand({ command: 'watch [src]', @@ -54,10 +52,7 @@ export default asyncCommand({ argv.https = ssl; } - let config = webpackConfig(argv); - await transformConfig(argv, config); - - let stats = await runWebpack(true, config, showStats); + let stats = await runWebpack(true, argv, showStats); showStats(stats); } }); diff --git a/src/lib/prerender.js b/src/lib/prerender.js deleted file mode 100644 index 63f8d234d..000000000 --- a/src/lib/prerender.js +++ /dev/null @@ -1,55 +0,0 @@ -import { resolve } from 'path'; -import createBabelConfig from './babel-config'; - -export default function prerender(config, params) { - params = params || {}; - - let entry = resolve(config.cwd, config.src || 'src', 'index.js'), - url = params.url || '/'; - - require('babel-register')({ - babelrc: false, - ignore: false, - ...createBabelConfig(config, { modules: 'commonjs' }) - }); - - global.location = { href:url, pathname:url }; - global.history = {}; - - // install CSS modules (just to generate correct classNames and support importing CSS & LESS) - require('css-modules-require-hook')({ - rootDir: resolve(config.cwd, config.src || 'src'), - generateScopedName: '[local]__[hash:base64:5]', - extensions: ['.less', '.css'], - processorOpts: { parser: require('postcss-less').parse } - }); - - // strip webpack loaders from import names - let { Module } = require('module'); - let oldResolve = Module._resolveFilename; - Module._resolveFilename = function (request, parent, isMain) { - request = request.replace(/^.*\!/g, ''); - return oldResolve.call(this, request, parent, isMain); - }; - - require('./polyfills'); - - let m = require(entry), - app = m && m.default || m; - - if (typeof app!=='function') { - // eslint-disable-next-line no-console - console.warn('Entry does not export a Component function/class, aborting prerendering.'); - return ''; - } - - let preact = require('preact'), - renderToString = require('preact-render-to-string'); - - let html = renderToString(preact.h(app, { url })); - - // restore resolution without loader stripping - Module._resolveFilename = oldResolve; - - return html; -} diff --git a/src/lib/async-component-loader.js b/src/lib/webpack/async-component-loader.js similarity index 96% rename from src/lib/async-component-loader.js rename to src/lib/webpack/async-component-loader.js index aaf2ca96a..3caf071a2 100644 --- a/src/lib/async-component-loader.js +++ b/src/lib/webpack/async-component-loader.js @@ -16,7 +16,7 @@ module.exports.pitch = function(remainingRequest) { } return ` - import async from ${JSON.stringify(path.resolve(__dirname, '../components/async'))}; + import async from ${JSON.stringify(path.resolve(__dirname, '../../components/async'))}; function load(cb) { require.ensure([], function(require) { diff --git a/src/lib/webpack/dummy-loader.js b/src/lib/webpack/dummy-loader.js new file mode 100644 index 000000000..5155a0702 --- /dev/null +++ b/src/lib/webpack/dummy-loader.js @@ -0,0 +1,3 @@ +module.exports = function(source, map) { + this.callback(null, source, map); +}; diff --git a/src/lib/npm-install-loader.js b/src/lib/webpack/npm-install-loader.js similarity index 100% rename from src/lib/npm-install-loader.js rename to src/lib/webpack/npm-install-loader.js diff --git a/src/lib/polyfills.js b/src/lib/webpack/polyfills.js similarity index 100% rename from src/lib/polyfills.js rename to src/lib/webpack/polyfills.js diff --git a/src/lib/webpack/prerender.js b/src/lib/webpack/prerender.js new file mode 100644 index 000000000..fb8c05534 --- /dev/null +++ b/src/lib/webpack/prerender.js @@ -0,0 +1,27 @@ +import { resolve } from 'path'; + +export default function prerender(outputDir, params) { + params = params || {}; + + let entry = resolve(outputDir, './ssr-build/ssr-bundle.js'), + url = params.url || '/'; + + global.location = { href:url, pathname:url }; + global.history = {}; + + let m = require(entry), + app = m && m.default || m; + + if (typeof app!=='function') { + // eslint-disable-next-line no-console + console.warn('Entry does not export a Component function/class, aborting prerendering.'); + return ''; + } + + let preact = require('preact'), + renderToString = require('preact-render-to-string'); + + let html = renderToString(preact.h(app, { url })); + + return html; +} diff --git a/src/lib/push-manifest.js b/src/lib/webpack/push-manifest.js similarity index 100% rename from src/lib/push-manifest.js rename to src/lib/webpack/push-manifest.js diff --git a/src/lib/run-webpack.js b/src/lib/webpack/run-webpack.js similarity index 61% rename from src/lib/run-webpack.js rename to src/lib/webpack/run-webpack.js index 951d4f856..601b5dded 100644 --- a/src/lib/run-webpack.js +++ b/src/lib/webpack/run-webpack.js @@ -1,25 +1,26 @@ -import { resolve } from 'path'; +import path from 'path'; import fs from 'fs.promised'; import webpack from 'webpack'; import WebpackDevServer from 'webpack-dev-server'; import chalk from 'chalk'; +import clientConfig from './webpack-client-config'; +import serverConfig from './webpack-server-config'; +import transformConfig from './transform-config'; -export default (watch=false, config, onprogress) => new Promise( (resolve, reject) => { - let compiler = webpack(config); +export default async (watch=false, env, onprogress) => { + if (watch) { + return await devBuild(env, onprogress); + } - let done = (err, stats) => { - if (err || stats.hasErrors()) { - reject(err || stats.toJson().errors.join('\n')); - } - else { - // Timeout for plugins that work on `after-emit` event of webpack - setTimeout(()=>{ - resolve(stats); - },20); - } - }; + return await prodBuild(env); +}; - if (watch) { +const devBuild = async (env, onprogress) => { + let config = clientConfig(env); + await transformConfig(env, config); + + let compiler = webpack(config); + return await new Promise((resolve, reject) => { let first = true; compiler.plugin('done', stats => { if (first) { @@ -34,11 +35,34 @@ export default (watch=false, config, onprogress) => new Promise( (resolve, rejec let server = new WebpackDevServer(compiler, config.devServer); server.listen(config.devServer.port); + }); +}; + +const prodBuild = async (env) => { + let compiler, client = clientConfig(env); + + await transformConfig(env, client); + + if (env.prerender) { + let ssrConfig = serverConfig(env); + await transformConfig(env, ssrConfig, true); + compiler = webpack([client, ssrConfig]); + } else { + compiler = webpack(client); } - else { - compiler.run(done); - } -}); + + return await new Promise((resolve, reject) => { + compiler.run((err, stats) => { + if (err || stats.hasErrors()) { + reject(err || stats.toJson().errors.join('\n')); + } + else { + // Timeout for plugins that work on `after-emit` event of webpack + setTimeout(()=> resolve(stats), 20); + } + }); + }); +}; export function showStats(stats) { let info = stats.toJson(); @@ -59,13 +83,15 @@ export function showStats(stats) { } export function writeJsonStats(stats) { - const outputPath = resolve(process.cwd(), 'stats.json'); - const jsonStats = stats.toJson({ + let outputPath = path.resolve(process.cwd(), 'stats.json'); + let jsonStats = stats.toJson({ json: true, chunkModules: true, source: false, }); + jsonStats = (jsonStats.children && jsonStats.children[0]) || jsonStats; + jsonStats.modules.forEach(normalizeModule); jsonStats.chunks.forEach(c => c.modules.forEach(normalizeModule)); diff --git a/src/lib/transform-config.js b/src/lib/webpack/transform-config.js similarity index 97% rename from src/lib/transform-config.js rename to src/lib/webpack/transform-config.js index ff7d7d114..5568aceb5 100644 --- a/src/lib/transform-config.js +++ b/src/lib/webpack/transform-config.js @@ -4,7 +4,7 @@ import { webpack, } from '@webpack-blocks/webpack2'; -export default async function (env, config) { +export default async function (env, config, ssr = false) { let transformerPath = path.resolve(env.cwd, env.config || './preact.config.js'); try { @@ -22,7 +22,7 @@ export default async function (env, config) { const m = require(transformerPath); const transformer = m && m.default || m; try { - await transformer(config, Object.assign({}, env), new WebpackConfigHelpers(env.cwd)); + await transformer(config, Object.assign({}, env, { ssr }), new WebpackConfigHelpers(env.cwd)); } catch (err) { throw new Error(`Error at ${transformerPath}: \n` + err); } diff --git a/src/lib/webpack-config.js b/src/lib/webpack/webpack-base-config.js similarity index 55% rename from src/lib/webpack-config.js rename to src/lib/webpack/webpack-base-config.js index e766f9eca..ccb356fd1 100644 --- a/src/lib/webpack-config.js +++ b/src/lib/webpack/webpack-base-config.js @@ -1,34 +1,21 @@ import { resolve } from 'path'; import { readFileSync, statSync } from 'fs'; -import { filter } from 'minimatch'; import { webpack, group, - createConfig, customConfig, setContext, - entryPoint, - setOutput, defineConstants, - performance, addPlugins, setDevTool } from '@webpack-blocks/webpack2'; -import devServer from '@webpack-blocks/dev-server2'; import ExtractTextPlugin from 'extract-text-webpack-plugin'; import autoprefixer from 'autoprefixer'; -import HtmlWebpackPlugin from 'html-webpack-plugin'; -import ScriptExtHtmlWebpackPlugin from 'script-ext-html-webpack-plugin'; -import HtmlWebpackExcludeAssetsPlugin from 'html-webpack-exclude-assets-plugin'; import ProgressBarPlugin from 'progress-bar-webpack-plugin'; -import CopyWebpackPlugin from 'copy-webpack-plugin'; import ReplacePlugin from 'webpack-plugin-replace'; -import SWPrecacheWebpackPlugin from 'sw-precache-webpack-plugin'; import requireRelative from 'require-relative'; -import prerender from './prerender'; -import PushManifestPlugin from './push-manifest'; -function exists(file) { +export function exists(file) { try { if (statSync(file)) return true; } catch (e) {} @@ -51,11 +38,8 @@ function resolveDep(dep, cwd) { return dep; } -export default env => { - let isProd = env && env.production; - let cwd = env.cwd = resolve(env.cwd || process.cwd()); - let src = dir => resolve(env.cwd, env.src || 'src', dir); - +export default (env) => { + let { isProd, cwd, src } = helpers(env); // only use src/ if it exists: if (!exists(src('.'))) { env.src = '.'; @@ -66,24 +50,13 @@ export default env => { let browsers = env.pkg.browserslist || ['> 1%', 'last 2 versions', 'IE >= 9']; - return createConfig.vanilla([ + return group([ setContext(src('.')), - entryPoint({ - 'bundle': resolve(__dirname, './entry'), - 'polyfills': resolve(__dirname, './polyfills'), - }), - setOutput({ - path: resolve(cwd, env.dest || 'build'), - publicPath: '/', - filename: '[name].js', - chunkFilename: '[name].chunk.[chunkhash:5].js' - }), - customConfig({ resolve: { modules: [ 'node_modules', - resolve(__dirname, '../../node_modules') + resolve(__dirname, '../../../node_modules') ], extensions: ['.js', '.jsx', '.ts', '.tsx', '.json', '.less', '.scss', '.sass', '.css'], alias: { @@ -98,11 +71,8 @@ export default env => { } }, resolveLoader: { - alias: { - 'async': resolve(__dirname, './async-component-loader') - }, modules: [ - resolve(__dirname, '../../node_modules'), + resolve(__dirname, '../../../node_modules'), resolve(cwd, 'node_modules') ] } @@ -119,7 +89,7 @@ export default env => { options: { babelrc: true, presets: [ - [resolve(__dirname, './babel-config'), { browsers }] + [resolve(__dirname, '../babel-config'), { browsers }] ] } } @@ -127,35 +97,6 @@ export default env => { } }), - // automatic async components :) - customConfig({ - module: { - loaders: [ - { - test: /\.jsx?$/, - include: [ - filter(src('routes')+'/{*.js,*/index.js}'), - filter(src('components')+'/{routes,async}/{*.js,*/index.js}') - ], - loader: resolve(__dirname, './async-component-loader'), - options: { - name(filename) { - let relative = filename.replace(src('.'), ''); - let isRoute = filename.indexOf('/routes/') >= 0; - - return isRoute ? 'route-' + relative.replace(/(^\/(routes|components\/(routes|async))\/|(\/index)?\.js$)/g, '') : false; - }, - formatName(filename) { - let relative = filename.replace(src('.'), ''); - // strip out context dir & any file/ext suffix - return relative.replace(/(^\/(routes|components\/(routes|async))\/|(\/index)?\.js$)/g, ''); - } - } - } - ] - } - }), - // LESS, SASS & CSS customConfig({ module: { @@ -261,13 +202,6 @@ export default env => { 'process.env.NODE_ENV': isProd ? 'production' : 'development' }), - // monitor output size and warn if it exceeds 200kb: - isProd && performance(Object.assign({ - maxAssetSize: 200 * 1000, - maxEntrypointSize: 200 * 1000, - hints: 'warning' - }, env.pkg.performance || {})), - // Source maps for dev/prod: setDevTool(isProd ? 'source-map' : 'cheap-module-eval-source-map'), @@ -283,28 +217,6 @@ export default env => { } }), - // copy any static files - addPlugins([ - new CopyWebpackPlugin([ - ...(exists(src('manifest.json')) ? [ - { from: 'manifest.json' } - ] : [ - { - from: resolve(__dirname, '../resources/manifest.json'), - to: 'manifest.json' - }, - { - from: resolve(__dirname, '../resources/icon.png'), - to: 'assets/icon.png' - } - ]), - exists(src('assets')) && { - from: 'assets', - to: 'assets' - } - ].filter(Boolean)) - ]), - // produce HTML & CSS: addPlugins([ new ExtractTextPlugin({ @@ -322,9 +234,7 @@ export default env => { // }) // ]), - htmlPlugin(env), - - isProd ? production(env) : development(env), + isProd ? production() : development(), addPlugins([ new webpack.NoEmitOnErrorsPlugin(), @@ -340,56 +250,14 @@ export default env => { async: false, children: true, minChunks: 3 - }), - - new PushManifestPlugin() + }) ]) ].filter(Boolean)); }; +const development = () => group([]); -const development = config => { - let port = process.env.PORT || config.port || 8080, - host = process.env.HOST || config.host || '0.0.0.0', - origin = `${config.https===true?'https':'http'}://${host}:${port}/`; - - return group([ - addPlugins([ - new webpack.NamedModulesPlugin() - ]), - - devServer({ - port, - host, - inline: true, - hot: true, - https: config.https, - compress: true, - publicPath: '/', - contentBase: resolve(config.cwd, config.src || './src'), - // setup(app) { - // app.use(middleware); - // }, - disableHostCheck: true, - historyApiFallback: true, - quiet: true, - clientLogLevel: 'none', - overlay: false, - stats: 'minimal', - watchOptions: { - ignored: [ - resolve(config.cwd, 'build'), - resolve(config.cwd, 'node_modules') - ] - } - }, [ - `webpack-dev-server/client?${origin}`, - `webpack/hot/dev-server?${origin}` - ]) - ]); -}; - -const production = config => addPlugins([ +const production = () => addPlugins([ new webpack.HashedModuleIdsPlugin(), new webpack.LoaderOptionsPlugin({ minimize: true @@ -441,48 +309,12 @@ const production = config => addPlugins([ ] } }), - - new SWPrecacheWebpackPlugin({ - filename: 'sw.js', - navigateFallback: 'index.html', - navigateFallbackWhitelist: [/^(?!\/__).*/], - minify: true, - stripPrefix: config.cwd, - staticFileGlobsIgnorePatterns: [ - /polyfills\.js$/, - /\.map$/, - /push-manifest\.json$/ - ] - }) ]); - -const htmlPlugin = config => addPlugins([ - new HtmlWebpackPlugin({ - filename: 'index.html', - template: `!!ejs-loader!${config.template || resolve(__dirname, '../resources/template.html')}`, - minify: config.production && { - collapseWhitespace: true, - removeScriptTypeAttributes: true, - removeRedundantAttributes: true, - removeStyleLinkTypeAttributes: true, - removeComments: true - }, - favicon: exists(resolve(config.src, 'assets/favicon.ico')) ? 'assets/favicon.ico' : resolve(__dirname, '../resources/favicon.ico'), - manifest: config.manifest, - inject: true, - compile: true, - preload: config.preload===true, - title: config.title || config.manifest.name || config.manifest.short_name || (config.pkg.name || '').replace(/^@[a-z]\//, '') || 'Preact App', - excludeAssets: [/(bundle|polyfills)(\..*)?\.js$/], - config, - ssr(params) { - return config.prerender ? prerender(config, params) : ''; - } - }), - new HtmlWebpackExcludeAssetsPlugin(), - new ScriptExtHtmlWebpackPlugin({ - // inline: 'bundle.js', - defaultAttribute: 'defer' - }) -]); +export function helpers(env) { + return { + isProd: env && env.production, + cwd: env.cwd = resolve(env.cwd || process.cwd()), + src: dir => resolve(env.cwd, env.src || 'src', dir) + }; +} diff --git a/src/lib/webpack/webpack-client-config.js b/src/lib/webpack/webpack-client-config.js new file mode 100644 index 000000000..88e023dc8 --- /dev/null +++ b/src/lib/webpack/webpack-client-config.js @@ -0,0 +1,195 @@ +import { resolve } from 'path'; +import { filter } from 'minimatch'; +import { + webpack, + createConfig, + customConfig, + entryPoint, + setOutput, + addPlugins, + performance, + group +} from '@webpack-blocks/webpack2'; +import devServer from '@webpack-blocks/dev-server2'; +import HtmlWebpackPlugin from 'html-webpack-plugin'; +import HtmlWebpackExcludeAssetsPlugin from 'html-webpack-exclude-assets-plugin'; +import ScriptExtHtmlWebpackPlugin from 'script-ext-html-webpack-plugin'; +import CopyWebpackPlugin from 'copy-webpack-plugin'; +import SWPrecacheWebpackPlugin from 'sw-precache-webpack-plugin'; +import PushManifestPlugin from './push-manifest'; +import baseConfig, { exists, helpers } from './webpack-base-config'; +import prerender from './prerender'; + +export default env => { + let { isProd, cwd, src } = helpers(env); + let outputDir = resolve(cwd, env.dest || 'build'); + return createConfig.vanilla([ + baseConfig(env), + entryPoint({ + 'bundle': resolve(__dirname, './../entry'), + 'polyfills': resolve(__dirname, './polyfills'), + }), + setOutput({ + path: outputDir, + publicPath: '/', + filename: '[name].js', + chunkFilename: '[name].chunk.[chunkhash:5].js', + }), + + // automatic async components :) + customConfig({ + module: { + loaders: [ + { + test: /\.jsx?$/, + include: [ + filter(src('routes')+'/{*.js,*/index.js}'), + filter(src('components')+'/{routes,async}/{*.js,*/index.js}') + ], + loader: resolve(__dirname, './async-component-loader'), + options: { + name(filename) { + let relative = filename.replace(src('.'), ''); + let isRoute = filename.indexOf('/routes/') >= 0; + + return isRoute ? 'route-' + relative.replace(/(^\/(routes|components\/(routes|async))\/|(\/index)?\.js$)/g, '') : false; + }, + formatName(filename) { + let relative = filename.replace(src('.'), ''); + // strip out context dir & any file/ext suffix + return relative.replace(/(^\/(routes|components\/(routes|async))\/|(\/index)?\.js$)/g, ''); + } + } + } + ] + } + }), + + // monitor output size and warn if it exceeds 200kb: + isProd && performance(Object.assign({ + maxAssetSize: 200 * 1000, + maxEntrypointSize: 200 * 1000, + hints: 'warning' + }, env.pkg.performance || {})), + // copy any static files + addPlugins([ + new CopyWebpackPlugin([ + ...(exists(src('manifest.json')) ? [ + { from: 'manifest.json' } + ] : [ + { + from: resolve(__dirname, '../../resources/manifest.json'), + to: 'manifest.json' + }, + { + from: resolve(__dirname, '../../resources/icon.png'), + to: 'assets/icon.png' + } + ]), + exists(src('assets')) && { + from: 'assets', + to: 'assets' + } + ].filter(Boolean)), + new PushManifestPlugin() + ]), + + htmlPlugin(env, outputDir), + + isProd ? production(env) : development(env), + + customConfig({ + resolveLoader: { + alias: { + 'async': resolve(__dirname, './async-component-loader') + }, + } + }) + ].filter(Boolean)); +}; + +const development = config => { + let port = process.env.PORT || config.port || 8080, + host = process.env.HOST || config.host || '0.0.0.0', + origin = `${config.https===true?'https':'http'}://${host}:${port}/`; + + return group([ + addPlugins([ + new webpack.NamedModulesPlugin() + ]), + + devServer({ + port, + host, + inline: true, + hot: true, + https: config.https===true, + compress: true, + publicPath: '/', + contentBase: resolve(config.cwd, config.src || './src'), + // setup(app) { + // app.use(middleware); + // }, + disableHostCheck: true, + historyApiFallback: true, + quiet: true, + clientLogLevel: 'none', + overlay: false, + stats: 'minimal', + watchOptions: { + ignored: [ + resolve(config.cwd, 'build'), + resolve(config.cwd, 'node_modules') + ] + } + }, [ + `webpack-dev-server/client?${origin}`, + `webpack/hot/dev-server?${origin}` + ]) + ]); +}; + +const production = config => addPlugins([ + new SWPrecacheWebpackPlugin({ + filename: 'sw.js', + navigateFallback: 'index.html', + navigateFallbackWhitelist: [/^(?!\/__).*/], + minify: true, + stripPrefix: config.cwd, + staticFileGlobsIgnorePatterns: [ + /polyfills(\..*)?\.js$/, + /\.map$/, + /push-manifest\.json$/ + ] + }) +]); + +const htmlPlugin = (config, outputDir) => addPlugins([ + new HtmlWebpackPlugin({ + filename: 'index.html', + template: `!!ejs-loader!${config.template || resolve(__dirname, '../../resources/template.html')}`, + minify: config.production && { + collapseWhitespace: true, + removeScriptTypeAttributes: true, + removeRedundantAttributes: true, + removeStyleLinkTypeAttributes: true, + removeComments: true + }, + favicon: exists(resolve(config.src, 'assets/favicon.ico')) ? 'assets/favicon.ico' : resolve(__dirname, '../../resources/favicon.ico'), + manifest: config.manifest, + inject: true, + compile: true, + preload: config.preload===true, + title: config.title || config.manifest.name || config.manifest.short_name || (config.pkg.name || '').replace(/^@[a-z]\//, '') || 'Preact App', + excludeAssets: [/(bundle|polyfills)(\..*)?\.js$/], + config, + ssr(params) { + return config.prerender ? prerender(outputDir, params) : ''; + } + }), + new HtmlWebpackExcludeAssetsPlugin(), + new ScriptExtHtmlWebpackPlugin({ + // inline: 'bundle.js', + defaultAttribute: 'defer' + }) +]); diff --git a/src/lib/webpack/webpack-server-config.js b/src/lib/webpack/webpack-server-config.js new file mode 100644 index 000000000..782e27602 --- /dev/null +++ b/src/lib/webpack/webpack-server-config.js @@ -0,0 +1,35 @@ +import { + createConfig, + entryPoint, + setOutput, + customConfig +} from '@webpack-blocks/webpack2'; +import { resolve } from 'path'; +import baseConfig, { helpers } from './webpack-base-config'; + +export default (env) => { + let { cwd } = helpers(env); + return createConfig.vanilla([ + baseConfig(env), + entryPoint(resolve(env.cwd, env.src || 'src', 'index.js')), + setOutput({ + path: resolve(cwd, env.dest || 'build', 'ssr-build'), + publicPath: '/', + filename: 'ssr-bundle.js', + chunkFilename: '[name].chunk.[chunkhash:5].js', + libraryTarget: 'commonjs2' + }), + + customConfig({ + target: 'node' + }), + + customConfig({ + resolveLoader: { + alias: { + 'async': resolve(__dirname, './dummy-loader') + }, + } + }) + ]); +}; diff --git a/tests/build.snapshot.js b/tests/build.snapshot.js index 2775942fd..30eb85049 100644 --- a/tests/build.snapshot.js +++ b/tests/build.snapshot.js @@ -1,3 +1,4 @@ + const smallBuildCommons = { assets: { 'favicon.ico': { size: 15086 }, @@ -38,6 +39,12 @@ export default { 'index.html': { size: 630 }, 'style.css': { size: 131 }, 'style.css.map': { size: 359 }, + 'ssr-build': { + 'ssr-bundle.js': { size: 9450 }, + 'ssr-bundle.js.map': { size: 42461 }, + 'style.css': { size: 130 }, + 'style.css.map': { size: 360 }, + } }, simple: { ...smallBuildCommons, @@ -46,6 +53,13 @@ export default { 'index.html': { size: 640 }, 'style.css': { size: 296}, 'style.css.map': { size: 621 }, + 'manifest.json': { size: 290 }, + 'ssr-build': { + 'ssr-bundle.js': { size: 10100 }, + 'ssr-bundle.js.map': { size: 46466 }, + 'style.css': { size: 296 }, + 'style.css.map': { size: 621 }, + } }, root: { ...fullBuildCommons, @@ -59,6 +73,12 @@ export default { 'index.html': { size: 870 }, 'style.css': { size: 1065 }, 'style.css.map': { size: 2246 }, + 'ssr-build': { + 'ssr-bundle.js': { size: 18960 }, + 'ssr-bundle.js.map': { size: 91773 }, + 'style.css': { size: 1065 }, + 'style.css.map': { size: 2250 }, + } }, 'default': { ...fullBuildCommons, @@ -72,9 +92,26 @@ export default { 'index.html': { size: 850 }, 'style.css': { size: 1065 }, 'style.css.map': { size: 2345 }, + 'ssr-build': { + 'ssr-bundle.js': { size: 19820 }, + 'ssr-bundle.js.map': { size: 95581 }, + 'style.css': { size: 1065 }, + 'style.css.map': { size: 2345 }, + } } }; +export const sassPrerendered = ` + +
+

Header on background

+

Paragraph on background

+
+ + {{ ... }} + +`; + export const withCustomTemplate = ` @@ -84,7 +121,8 @@ export const withCustomTemplate = ` -

This is an app with custom template

+

Guess what

+

This is an app with custom template

diff --git a/tests/build.test.js b/tests/build.test.js index 64ee547e5..f03e37006 100644 --- a/tests/build.test.js +++ b/tests/build.test.js @@ -5,7 +5,7 @@ import htmlLooksLike from 'html-looks-like'; import { create, build } from './lib/cli'; import lsr from './lib/lsr'; import { setup, clean, fromSubject } from './lib/output'; -import expectedOutputs, { withCustomTemplate } from './build.snapshot'; +import expectedOutputs, { sassPrerendered, withCustomTemplate } from './build.snapshot'; import filesMatchSnapshot from './lib/filesMatchSnapshot'; const options = { timeout: 45 * 1000 }; @@ -25,13 +25,22 @@ test('preact build - before', async () => { }) ); +test(`preact build - should prerender using webpack.`, options, async t => { + let app = await fromSubject('sass'); + await build(app); + + let output = await fs.readFile(resolve(app, './build/index.html'), 'utf-8'); + let html = output.match(/.*<\/body>/)[0]; + htmlLooksLike(html, sassPrerendered); + t.pass(); +}); + test(`preact build - should use custom .babelrc.`, options, async t => { // app with custom .babelrc enabling async functions let app = await fromSubject('custom-babelrc'); // UglifyJS throws error when generator is encountered - // TODO: Remove '--no-prederender' once #71 is merged - babel-register problem - await build(app, ['--no-prerender']); + await build(app); t.pass(); }); @@ -40,8 +49,7 @@ test(`preact build - should use custom preact.config.js.`, options, async t => { // app with custom template set via preact.config.js let app = await fromSubject('custom-webpack'); - // TODO: Remove '--no-prederender' once #71 is merged - babel-register problem - await build(app, ['--no-prerender']); + await build(app); let html = await fs.readFile(resolve(app, './build/index.html'), 'utf-8'); htmlLooksLike(html, withCustomTemplate); diff --git a/tests/lib/cli.js b/tests/lib/cli.js index 5841b99a9..a19c74f03 100644 --- a/tests/lib/cli.js +++ b/tests/lib/cli.js @@ -13,8 +13,8 @@ export const create = async (appName, template) => { return resolve(workDir, appName); }; -export const build = async (appDir, args = []) => { - await run(['build', ...args], appDir); +export const build = async (appDir) => { + await run(['build'], appDir); }; export const serve = async (appDir, port) => { diff --git a/tests/subjects/custom-babelrc/.babelrc b/tests/subjects/custom-babelrc/.babelrc index 2d12be1f5..fa5b8e976 100644 --- a/tests/subjects/custom-babelrc/.babelrc +++ b/tests/subjects/custom-babelrc/.babelrc @@ -1,5 +1,10 @@ { "plugins": [ - "transform-regenerator" + "transform-regenerator", + ["transform-runtime", { + "helpers": false, + "polyfill": false, + "regenerator": true + }] ] } diff --git a/tests/subjects/custom-webpack/index.js b/tests/subjects/custom-webpack/index.js index bc32cc189..bfe72fb3a 100644 --- a/tests/subjects/custom-webpack/index.js +++ b/tests/subjects/custom-webpack/index.js @@ -1,3 +1,3 @@ import { h } from 'preact'; -export default () => (

Hello

); +export default () => (

This is an app with custom template

); diff --git a/tests/subjects/custom-webpack/preact.config.js b/tests/subjects/custom-webpack/preact.config.js index 8e9e53e7e..a0ea82f67 100644 --- a/tests/subjects/custom-webpack/preact.config.js +++ b/tests/subjects/custom-webpack/preact.config.js @@ -1,6 +1,9 @@ import path from 'path'; export default (config, env, helpers) => { - const { plugin: htmlWebpackPlugin } = helpers.getPluginsByName(config, 'HtmlWebpackPlugin')[0]; + if (env.ssr) { + return; + } + let { plugin: htmlWebpackPlugin } = helpers.getPluginsByName(config, 'HtmlWebpackPlugin')[0]; htmlWebpackPlugin.options.template = `!!ejs-loader!${path.resolve(__dirname, './template.html')}`; }; diff --git a/tests/subjects/custom-webpack/template.html b/tests/subjects/custom-webpack/template.html index 802615d95..a16b655e8 100644 --- a/tests/subjects/custom-webpack/template.html +++ b/tests/subjects/custom-webpack/template.html @@ -5,7 +5,7 @@ <%= htmlWebpackPlugin.options.title %> -

This is an app with custom template

+

Guess what

<%= htmlWebpackPlugin.options.ssr({ url: '/' }) %> diff --git a/tests/subjects/sass/components/app/index.js b/tests/subjects/sass/components/app/index.js new file mode 100644 index 000000000..0b8a44f98 --- /dev/null +++ b/tests/subjects/sass/components/app/index.js @@ -0,0 +1,11 @@ +import { h } from 'preact'; +import style from './style.scss'; + +const App = () => ( +
+

Header on background

+

Paragraph on background

+
+); + +export default App; diff --git a/tests/subjects/sass/components/app/style.scss b/tests/subjects/sass/components/app/style.scss new file mode 100644 index 000000000..cddec2546 --- /dev/null +++ b/tests/subjects/sass/components/app/style.scss @@ -0,0 +1,3 @@ +.background { + background: teal; +} diff --git a/tests/subjects/sass/index.js b/tests/subjects/sass/index.js new file mode 100644 index 000000000..671140136 --- /dev/null +++ b/tests/subjects/sass/index.js @@ -0,0 +1,4 @@ +import { h } from 'preact'; +import App from './components/app'; + +export default () => ();