Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
- Fully **automatic code splitting** for routes
- Transparently code-split any component with an [`async!`] prefix
- Auto-generated [Service Workers] for offline caching powered by [sw-precache]
- [PRPL](https://developers.google.com/web/fundamentals/performance/prpl-pattern/) pattern support for efficient loading
- [PRPL] pattern support for efficient loading
- Zero-configuration pre-rendering / server-side rendering hydration
- Support for CSS Modules, LESS, Sass, Stylus; with Autoprefixer
- Monitor your bundle/chunk sizes with built-in tracking
Expand Down Expand Up @@ -106,6 +106,14 @@ npm run serve -- --server config
# Copy your static files to a server!
```

### Pre-rendering

Preact CLI in order to follow [PRPL] pattern renders initial route (`/`) into generated static `index.html` - this ensures that users get to see your page before any JavaScript is run, and thus providing users with slow devices or poor connection your website's content much faster.

Preact CLI does this by rendering your app inside node - this means that we don't have access to DOM or other global variables available in browsers, similar how it would be in server-side rendering scenarios. In case you need to rely on browser APIs you could:
- drop out of prerendering by passing `--no-prerender` flag to `preact build`,
- write your code in a way that supports server-side rendering by wrapping code that requires browser's APIs in conditional statements `if (typeof window !== "undefined") { ... }` ensuring that on server those lines of code are never reached. Alternatively you could use a helper library like [window-or-global](https://www.npmjs.com/package/window-or-global).

### Custom Configuration

#### Browserslist
Expand Down Expand Up @@ -189,3 +197,4 @@ preact watch --template src/template.html
[```.babelrc```]: https://babeljs.io/docs/usage/babelrc/
[Preact CLI preset]: https://github.com/developit/preact-cli/blob/master/src/lib/babel-config.js
[WebpackConfigHelpers]: docs/webpack-helpers.md
[PRPL]: https://developers.google.com/web/fundamentals/performance/prpl-pattern/
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@
"rimraf": "^2.6.1",
"script-ext-html-webpack-plugin": "^1.8.0",
"simplehttp2server": "^2.0.0",
"source-map": "^0.5.6",
"stack-trace": "0.0.10",
"sw-precache-webpack-plugin": "^0.11.2",
"tmp": "0.0.31",
"unfetch": "^3.0.0",
Expand Down
80 changes: 68 additions & 12 deletions src/lib/webpack/prerender.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,83 @@
import { resolve } from 'path';
import fs from 'fs';
import stackTrace from 'stack-trace';
import { SourceMapConsumer } from 'source-map';
import chalk from 'chalk';

export default function prerender(outputDir, params) {
export default function prerender(env, params) {
params = params || {};

let entry = resolve(outputDir, './ssr-build/ssr-bundle.js'),
let entry = resolve(env.dest, './ssr-build/ssr-bundle.js'),
url = params.url || '/';

global.location = { href:url, pathname:url };
global.history = {};

let m = require(entry),
try {
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 '';
}
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 preact = require('preact'),
renderToString = require('preact-render-to-string');

let html = renderToString(preact.h(app, { url }));
return renderToString(preact.h(app, { url }));
} catch (err) {
let stack = stackTrace.parse(err).filter(s => s.getFileName() === entry)[0];
if (!stack) {
throw err;
}

return html;
handlePrerenderError(err, env, stack, entry);
}
}

const handlePrerenderError = (err, env, stack, entry) => {
let errorMessage = err.toString();
let isReferenceError = errorMessage.startsWith('ReferenceError');
let sourceMapContent = JSON.parse(fs.readFileSync(`${entry}.map`));
let sourceMapConsumer = new SourceMapConsumer(sourceMapContent);
let position = sourceMapConsumer.originalPositionFor({
line: stack.getLineNumber(),
column: stack.getColumnNumber()
});

position.source = position.source.replace('webpack://', '.');

let sourcePath = resolve(env.src, position.source);
let sourceLines = fs.readFileSync(sourcePath, 'utf-8').split('\n');
let methodName = stack.getMethodName();
let sourceCodeHighlight = '';

for (var i = -4; i <= 4; i++) {
let color = i === 0 ? chalk.red : chalk.yellow;
let line = position.line + i;
let sourceLine = sourceLines[line - 1];
sourceCodeHighlight += sourceLine ? `${color(sourceLine)}\n` : '';
}

process.stderr.write('\n');
process.stderr.write(chalk.red(`${errorMessage}\n`));
process.stderr.write(`method: ${methodName}\n`);
process.stderr.write(`at: ${sourcePath}:${position.line}:${position.column}\n`);
process.stderr.write('\n');
process.stderr.write('Source code:\n\n');
process.stderr.write(sourceCodeHighlight);
process.stderr.write('\n');
process.stderr.write(`This ${isReferenceError ? 'is most likely' : 'could be'} caused by using DOM or Web APIs.\n`);
process.stderr.write(`Pre-render runs in node and has no access to globals available in browsers.\n\n`);
process.stderr.write(`Consider wrapping code producing error in: 'if (typeof window !== "undefined") { ... }'\n`);

if (methodName === 'componentWillMount') {
process.stderr.write(`or place logic in 'componentDidMount' method.\n`);
}
process.stderr.write('\n');
process.stderr.write(`Alternatively use 'preact build --no-prerender' to disable prerendering.\n\n`);
process.stderr.write('See https://github.com/developit/preact-cli#pre-rendering for further information.');
process.exit(1);
};
39 changes: 1 addition & 38 deletions src/lib/webpack/webpack-base-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export default (env) => {
env.src = '.';
}

env.dest = resolve(cwd, env.dest || 'build');
env.manifest = readJson(src('manifest.json')) || {};
env.pkg = readJson(resolve(cwd, 'package.json')) || {};

Expand Down Expand Up @@ -288,44 +289,6 @@ const production = () => addPlugins([
value: s => `return;${ Array(s.length-7).join(' ') }(`
}]
}),

new webpack.optimize.UglifyJsPlugin({
output: {
comments: false
},
mangle: true,
sourceMap: true,
compress: {
properties: true,
keep_fargs: false,
pure_getters: true,
collapse_vars: true,
warnings: false,
screw_ie8: true,
sequences: true,
dead_code: true,
drop_debugger: true,
comparisons: true,
conditionals: true,
evaluate: true,
booleans: true,
loops: true,
unused: true,
hoist_funs: true,
if_return: true,
join_vars: true,
cascade: true,
drop_console: false,
pure_funcs: [
'classCallCheck',
'_classCallCheck',
'_possibleConstructorReturn',
'Object.freeze',
'invariant',
'warning'
]
}
}),
]);

export function helpers(env) {
Expand Down
51 changes: 44 additions & 7 deletions src/lib/webpack/webpack-client-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,16 @@ import baseConfig, { exists, readJson, 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');
let { isProd, src } = helpers(env);

return createConfig.vanilla([
baseConfig(env),
entryPoint({
'bundle': resolve(__dirname, './../entry'),
'polyfills': resolve(__dirname, './polyfills'),
}),
setOutput({
path: outputDir,
path: env.dest,
publicPath: '/',
filename: '[name].js',
chunkFilename: '[name].chunk.[chunkhash:5].js',
Expand Down Expand Up @@ -94,7 +94,7 @@ export default env => {
new PushManifestPlugin()
]),

htmlPlugin(env, outputDir),
htmlPlugin(env, src('.')),

isProd ? production(env) : development(env),

Expand Down Expand Up @@ -150,6 +150,43 @@ const development = config => {
};

const production = config => addPlugins([
new webpack.optimize.UglifyJsPlugin({
output: {
comments: false
},
mangle: true,
sourceMap: true,
compress: {
properties: true,
keep_fargs: false,
pure_getters: true,
collapse_vars: true,
warnings: false,
screw_ie8: true,
sequences: true,
dead_code: true,
drop_debugger: true,
comparisons: true,
conditionals: true,
evaluate: true,
booleans: true,
loops: true,
unused: true,
hoist_funs: true,
if_return: true,
join_vars: true,
cascade: true,
drop_console: false,
pure_funcs: [
'classCallCheck',
'_classCallCheck',
'_possibleConstructorReturn',
'Object.freeze',
'invariant',
'warning'
]
}
}),
new SWPrecacheWebpackPlugin({
filename: 'sw.js',
navigateFallback: 'index.html',
Expand All @@ -164,9 +201,9 @@ const production = config => addPlugins([
})
]);

const htmlPlugin = (config, outputDir) => {
const htmlPlugin = (config, src) => {
const htmlWebpackConfig = ({ url, title }) => ({
filename: resolve(outputDir, url.substring(1), 'index.html'),
filename: resolve(config.dest, url.substring(1), 'index.html'),
template: `!!ejs-loader!${config.template || resolve(__dirname, '../../resources/template.html')}`,
minify: config.production && {
collapseWhitespace: true,
Expand All @@ -184,7 +221,7 @@ const htmlPlugin = (config, outputDir) => {
excludeAssets: [/(bundle|polyfills)(\..*)?\.js$/],
config,
ssr(params) {
return config.prerender ? prerender(outputDir, { ...params, url }) : '';
return config.prerender ? prerender({ dest: config.dest, src }, { ...params, url }) : '';
}
});
const pages = readJson(resolve(config.cwd, config.prerenderUrls || '')) || [{ url: "/" }];
Expand Down
5 changes: 2 additions & 3 deletions src/lib/webpack/webpack-server-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@ import {
customConfig
} from '@webpack-blocks/webpack2';
import { resolve } from 'path';
import baseConfig, { helpers } from './webpack-base-config';
import baseConfig 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'),
path: resolve(env.dest, 'ssr-build'),
publicPath: '/',
filename: 'ssr-bundle.js',
chunkFilename: '[name].chunk.[chunkhash:5].js',
Expand Down
16 changes: 8 additions & 8 deletions tests/build.snapshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ export default {
'style.css': { size: 131 },
'style.css.map': { size: 359 },
'ssr-build': {
'ssr-bundle.js': { size: 9450 },
'ssr-bundle.js.map': { size: 42461 },
'ssr-bundle.js': { size: 16245 },
'ssr-bundle.js.map': { size: 31821 },
'style.css': { size: 130 },
'style.css.map': { size: 360 },
}
Expand All @@ -55,8 +55,8 @@ export default {
'style.css.map': { size: 621 },
'manifest.json': { size: 290 },
'ssr-build': {
'ssr-bundle.js': { size: 10100 },
'ssr-bundle.js.map': { size: 46466 },
'ssr-bundle.js': { size: 18205 },
'ssr-bundle.js.map': { size: 33478 },
'style.css': { size: 296 },
'style.css.map': { size: 621 },
}
Expand All @@ -74,8 +74,8 @@ export default {
'style.css': { size: 1065 },
'style.css.map': { size: 2246 },
'ssr-build': {
'ssr-bundle.js': { size: 18960 },
'ssr-bundle.js.map': { size: 97403 },
'ssr-bundle.js': { size: 39459 },
'ssr-bundle.js.map': { size: 65629 },
'style.css': { size: 1065 },
'style.css.map': { size: 2250 },
}
Expand All @@ -93,8 +93,8 @@ export default {
'style.css': { size: 1065 },
'style.css.map': { size: 2345 },
'ssr-build': {
'ssr-bundle.js': { size: 19820 },
'ssr-bundle.js.map': { size: 101502 },
'ssr-bundle.js': { size: 41715 },
'ssr-bundle.js.map': { size: 66661 },
'style.css': { size: 1065 },
'style.css.map': { size: 2345 },
}
Expand Down