Compare commits

..

4 Commits

Author SHA1 Message Date
Rich Harris
2b799a3f1e print warnings in dev mode for unused data 2018-08-03 01:14:26 -04:00
Rich Harris
18d15c0120 emit events for unused data, and only in dev/export mode 2018-08-03 01:14:08 -04:00
Rich Harris
b20e15721c Merge branch 'master' into proxy-data 2018-08-03 00:15:26 -04:00
Rich Harris
06cc22b10d detect unused data on initial render 2018-07-31 23:21:34 -04:00
15 changed files with 231 additions and 104 deletions

View File

@@ -18,4 +18,4 @@ addons:
install: install:
- export DISPLAY=':99.0' - export DISPLAY=':99.0'
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
- npm ci || npm i - npm install

View File

@@ -1,14 +1,5 @@
# sapper changelog # sapper changelog
## 0.15.7
* Strip leading slash from redirects ([#291](https://github.com/sveltejs/sapper/issues/291))
* Pass `(req, res)` to store getter ([#344](https://github.com/sveltejs/sapper/issues/344))
## 0.15.6
* Fix exporting with custom basepath ([#342](https://github.com/sveltejs/sapper/pull/342))
## 0.15.5 ## 0.15.5
* Faster `export` with more explanatory output ([#335](https://github.com/sveltejs/sapper/pull/335)) * Faster `export` with more explanatory output ([#335](https://github.com/sveltejs/sapper/pull/335))

View File

@@ -14,7 +14,7 @@ environment:
install: install:
- ps: Install-Product node $env:nodejs_version - ps: Install-Product node $env:nodejs_version
- npm ci - npm install
test_script: test_script:
- node --version && npm --version - node --version && npm --version

View File

@@ -1,6 +1,6 @@
{ {
"name": "sapper", "name": "sapper",
"version": "0.15.7", "version": "0.15.4",
"description": "Military-grade apps, engineered by Svelte", "description": "Military-grade apps, engineered by Svelte",
"main": "dist/middleware.ts.js", "main": "dist/middleware.ts.js",
"bin": { "bin": {

View File

@@ -224,11 +224,8 @@ class Watcher extends EventEmitter {
}); });
this.proc.on('message', message => { this.proc.on('message', message => {
if (message.__sapper__ && message.event === 'basepath') { if (!message.__sapper__) return;
this.emit('basepath', { this.emit(message.event, message);
basepath: message.basepath
});
}
}); });
this.proc.on('exit', emitFatal); this.proc.on('exit', emitFatal);

View File

@@ -51,10 +51,9 @@ async function execute(emitter: EventEmitter, {
const port = await ports.find(3000); const port = await ports.find(3000);
const origin = `http://localhost:${port}`; const origin = `http://localhost:${port}`;
const root = new URL(basepath || '', origin);
emitter.emit('info', { emitter.emit('info', {
message: `Crawling ${root.href}` message: `Crawling ${origin}`
}); });
const proc = child_process.fork(path.resolve(`${build}/server.js`), [], { const proc = child_process.fork(path.resolve(`${build}/server.js`), [], {
@@ -72,8 +71,6 @@ async function execute(emitter: EventEmitter, {
const deferreds = new Map(); const deferreds = new Map();
function get_deferred(pathname: string) { function get_deferred(pathname: string) {
pathname = pathname.replace(root.pathname, '');
if (!deferreds.has(pathname)) { if (!deferreds.has(pathname)) {
deferreds.set(pathname, new Deferred()) ; deferreds.set(pathname, new Deferred()) ;
} }
@@ -110,7 +107,7 @@ async function execute(emitter: EventEmitter, {
}); });
async function handle(url: URL) { async function handle(url: URL) {
const pathname = (url.pathname.replace(root.pathname, '') || '/'); const pathname = url.pathname || '/';
if (seen.has(pathname)) return; if (seen.has(pathname)) return;
seen.add(pathname); seen.add(pathname);
@@ -141,9 +138,6 @@ async function execute(emitter: EventEmitter, {
} }
return ports.wait(port) return ports.wait(port)
.then(() => { .then(() => handle(new URL(`/${basepath}`, origin))) // TODO all static routes
// TODO all static routes
return handle(root);
})
.then(() => proc.kill()); .then(() => proc.kill());
} }

View File

@@ -2,6 +2,7 @@ import * as path from 'path';
import * as colors from 'ansi-colors'; import * as colors from 'ansi-colors';
import * as child_process from 'child_process'; import * as child_process from 'child_process';
import prettyMs from 'pretty-ms'; import prettyMs from 'pretty-ms';
import pb from 'pretty-bytes';
import { dev as _dev } from '../api/dev'; import { dev as _dev } from '../api/dev';
import * as events from '../api/interfaces'; import * as events from '../api/interfaces';
@@ -44,6 +45,32 @@ export function dev(opts: { port: number, open: boolean }) {
if (event.log) console.log(event.log); if (event.log) console.log(event.log);
}); });
watcher.on('preload', (event) => {
if (event.size > 25000) {
console.log(colors.bold.yellow(`${event.url} — large amount of preloaded data`));
console.log(`${colors.bold(pb(event.size))} of data was preloaded in total, above the recommended limit of ${pb(25000)}`);
}
});
watcher.on('unused_data', (event) => {
console.log(colors.bold.yellow(`${event.url} — unused preloaded data`));
console.log(`More data was returned from \`preload\` than was used during the initial render. Consider only returning essential data.`);
event.discrepancies.forEach(discrepancy => {
console.log(`${colors.bold(discrepancy.file)} loaded ${colors.bold(pb(discrepancy.preloaded))}, of which ${discrepancy.rendered > 2 ? `only ${colors.bold(pb(discrepancy.rendered))}` : 'none'} was used. The following properties were not referenced:`);
const slice = discrepancy.props.length > 12
? discrepancy.props.slice(0, 10)
: discrepancy.props;
console.log(slice.map((prop: string) => `${prop}`).join('\n'));
if (discrepancy.props.length > slice.length) {
console.log(`...and ${discrepancy.props.length - slice.length} more`);
}
});
});
watcher.on('build', (event: events.BuildEvent) => { watcher.on('build', (event: events.BuildEvent) => {
if (event.errors.length) { if (event.errors.length) {
console.log(`${colors.bold.red(`${event.type}`)}`); console.log(`${colors.bold.red(`${event.type}`)}`);

View File

@@ -156,6 +156,7 @@ function generate_server(
const props = [ const props = [
`name: "${part.component.name}"`, `name: "${part.component.name}"`,
`file: "${part.component.file}"`,
`component: ${part.component.name}` `component: ${part.component.name}`
]; ];

View File

@@ -8,6 +8,9 @@ import fetch from 'node-fetch';
import { lookup } from './middleware/mime'; import { lookup } from './middleware/mime';
import { locations, dev } from './config'; import { locations, dev } from './config';
import sourceMapSupport from 'source-map-support'; import sourceMapSupport from 'source-map-support';
import prettyBytes from 'pretty-bytes';
import { wrap_data } from './middleware/wrap_data';
import { list_unused_properties } from './middleware/list_unused_properties';
sourceMapSupport.install(); sourceMapSupport.install();
@@ -83,7 +86,7 @@ function toIgnore(uri: string, val: any) {
export default function middleware(opts: { export default function middleware(opts: {
manifest: Manifest, manifest: Manifest,
store: (req: Req, res: ServerResponse) => Store, store: (req: Req) => Store,
ignore?: any, ignore?: any,
routes?: any // legacy routes?: any // legacy
}) { }) {
@@ -276,10 +279,7 @@ function get_server_route_handler(routes: ServerRoute[]) {
}; };
} }
function get_page_handler( function get_page_handler(manifest: Manifest, store_getter: (req: Req) => Store) {
manifest: Manifest,
store_getter: (req: Req, res: ServerResponse) => Store
) {
const output = locations.dest(); const output = locations.dest();
const get_chunks = dev() const get_chunks = dev()
@@ -293,6 +293,8 @@ function get_page_handler(
const { server_routes, pages } = manifest; const { server_routes, pages } = manifest;
const error_route = manifest.error; const error_route = manifest.error;
const should_wrap_data = dev() || process.env.SAPPER_EXPORT;
function handle_error(req: Req, res: ServerResponse, statusCode: number, error: Error | string) { function handle_error(req: Req, res: ServerResponse, statusCode: number, error: Error | string) {
handle_page({ handle_page({
pattern: null, pattern: null,
@@ -329,7 +331,7 @@ function get_page_handler(
res.setHeader('Link', link); res.setHeader('Link', link);
const store = store_getter ? store_getter(req, res) : null; const store = store_getter ? store_getter(req) : null;
let redirect: { statusCode: number, location: string }; let redirect: { statusCode: number, location: string };
let preload_error: { statusCode: number, message: Error | string }; let preload_error: { statusCode: number, message: Error | string };
@@ -339,7 +341,6 @@ function get_page_handler(
if (redirect && (redirect.statusCode !== statusCode || redirect.location !== location)) { if (redirect && (redirect.statusCode !== statusCode || redirect.location !== location)) {
throw new Error(`Conflicting redirects`); throw new Error(`Conflicting redirects`);
} }
location = location.replace(/^\//g, ''); // leading slash (only)
redirect = { statusCode, location }; redirect = { statusCode, location };
}, },
error: (statusCode: number, message: Error | string) => { error: (statusCode: number, message: Error | string) => {
@@ -432,11 +433,6 @@ function get_page_handler(
return; return;
} }
const serialized = {
preloaded: `[${preloaded.map(data => try_serialize(data)).join(',')}]`,
store: store && try_serialize(store.get())
};
const segments = req.path.split('/').filter(Boolean); const segments = req.path.split('/').filter(Boolean);
const props: Props = { const props: Props = {
@@ -458,6 +454,15 @@ function get_page_handler(
} }
}); });
// in dev and export modes, we wrap data in proxies to see
// how much of it is used in the initial render
const wrapped = should_wrap_data && wrap_data(preloaded);
// this is an easy way to 'reify' top-level values
const _preloaded = should_wrap_data
? wrapped.data.map((x: any) => x)
: preloaded;
let level = data.child; let level = data.child;
for (let i = 0; i < page.parts.length; i += 1) { for (let i = 0; i < page.parts.length; i += 1) {
const part = page.parts[i]; const part = page.parts[i];
@@ -469,7 +474,7 @@ function get_page_handler(
component: part.component, component: part.component,
props: Object.assign({}, props, { props: Object.assign({}, props, {
params: get_params(match) params: get_params(match)
}, preloaded[i + 1]) }, _preloaded[i + 1])
}); });
level.props.child = <Props["child"]>{ level.props.child = <Props["child"]>{
@@ -488,6 +493,47 @@ function get_page_handler(
.map(file => `<script src='${req.baseUrl}/client/${file}'></script>`) .map(file => `<script src='${req.baseUrl}/client/${file}'></script>`)
.join(''); .join('');
const unwrapped = should_wrap_data && wrapped.unwrap();
const preloaded_serialized = preloaded.map(try_serialize);
if (should_wrap_data && process.send) {
const discrepancies = [];
unwrapped.forEach((clone, i) => {
const loaded = preloaded_serialized[i];
if (!loaded) return;
const rendered = try_serialize(clone);
if (rendered !== loaded) {
const part = page.parts[i - 1];
const file = part ? part.file : '_layout.html';
discrepancies.push({
file,
preloaded: loaded.length,
rendered: rendered.length,
props: list_unused_properties(preloaded[i], clone)
});
}
});
if (discrepancies.length) {
process.send({
__sapper__: true,
event: 'unused_data',
url: req.url,
discrepancies
});
}
}
const serialized = {
preloaded: `[${preloaded_serialized.join(',')}]`,
store: store && try_serialize(store.get())
};
let inline_script = `__SAPPER__={${[ let inline_script = `__SAPPER__={${[
error && `error:1`, error && `error:1`,
`baseUrl:"${req.baseUrl}"`, `baseUrl:"${req.baseUrl}"`,
@@ -511,6 +557,13 @@ function get_page_handler(
res.end(body); res.end(body);
if (process.send) { if (process.send) {
process.send({
__sapper__: true,
event: 'preload',
url: req.url,
size: serialized.preloaded.length
});
process.send({ process.send({
__sapper__: true, __sapper__: true,
event: 'file', event: 'file',

View File

@@ -0,0 +1,34 @@
export function list_unused_properties(all: any, used: any) {
const props: string[] = [];
const seen = new Set();
function walk(keypath: string, a: any, b: any) {
if (seen.has(a)) return;
seen.add(a);
if (!a || typeof a !== 'object') return;
const is_array = Array.isArray(a);
for (const key in a) {
const child_keypath = keypath
? is_array ? `${keypath}[${key}]` : `${keypath}.${key}`
: key;
if (hasProp.call(b, key)) {
const a_child = a[key];
const b_child = b[key];
walk(child_keypath, a_child, b_child);
} else {
props.push(child_keypath);
}
}
}
walk(null, all, used);
return props;
}
const hasProp = Object.prototype.hasOwnProperty;

View File

@@ -0,0 +1,85 @@
type Obj = Record<string, any>;
export function wrap_data(data: any) {
const proxies = new Map();
const clones = new Map();
const handler = {
get(target: any, property: string): any {
const value = target[property];
const intercepted = intercept(value);
const target_clone = clones.get(target);
const child_clone = clones.get(value);
if (target_clone && target.hasOwnProperty(property)) {
target_clone[property] = child_clone || value;
}
return intercepted;
},
};
function get_or_create_proxy(obj: any) {
if (!proxies.has(obj)) {
proxies.set(obj, new Proxy(obj, handler));
}
return proxies.get(obj);
}
function intercept(obj: any) {
if (clones.has(obj)) return obj;
if (obj && typeof obj === 'object') {
if (Array.isArray(obj)) {
clones.set(obj, []);
return get_or_create_proxy(obj);
}
else if (isPlainObject(obj)) {
clones.set(obj, {});
return get_or_create_proxy(obj);
}
}
clones.set(obj, obj);
return obj;
}
return {
data: intercept(data),
unwrap: () => {
return clones.get(data);
}
};
}
const objectProtoOwnPropertyNames = Object.getOwnPropertyNames(Object.prototype).sort().join('\0')
function isPlainObject(obj: any) {
const proto = Object.getPrototypeOf(obj);
if (
proto !== Object.prototype &&
proto !== null &&
Object.getOwnPropertyNames(proto).sort().join('\0') !== objectProtoOwnPropertyNames
) {
return false;
}
if (Object.getOwnPropertySymbols(obj).length > 0) {
return false;
}
return true;
}
function pick(obj: Obj, props: string[]) {
const picked: Obj = {};
props.forEach(prop => {
picked[prop] = obj[prop];
});
return picked;
}

View File

@@ -85,18 +85,11 @@ const middlewares = [
next(); next();
}, },
// set up some values for the store
(req, res, next) => {
req.hello = 'hello';
res.locals = { name: 'world' };
next();
},
sapper({ sapper({
manifest, manifest,
store: (req, res) => { store: () => {
return new Store({ return new Store({
title: `${req.hello} ${res.locals.name}` title: 'Stored title'
}); });
}, },
ignore: [ ignore: [

View File

@@ -8,7 +8,6 @@
<a href='about'>about</a> <a href='about'>about</a>
<a href='slow-preload'>slow preload</a> <a href='slow-preload'>slow preload</a>
<a href='redirect-from'>redirect</a> <a href='redirect-from'>redirect</a>
<a href='redirect-root'>redirect (root)</a>
<a href='blog/nope'>broken link</a> <a href='blog/nope'>broken link</a>
<a href='blog/throw-an-error'>error link</a> <a href='blog/throw-an-error'>error link</a>
<a href='credentials?creds=include'>credentials</a> <a href='credentials?creds=include'>credentials</a>

View File

@@ -1,7 +0,0 @@
<script>
export default {
preload() {
this.redirect(301, '/');
}
};
</script>

View File

@@ -59,19 +59,9 @@ describe('sapper', function() {
basepath: '/custom-basepath' basepath: '/custom-basepath'
}); });
testExport({}); describe('export', () => {
testExport({ basepath: '/custom-basepath' });
});
function testExport({ basepath = '' }) {
describe(basepath ? `export --basepath ${basepath}` : 'export', () => {
before(() => { before(() => {
if (basepath) { return exec(`node ${cli} export`);
process.env.BASEPATH = basepath;
}
return exec(`node ${cli} export ${basepath ? `--basepath ${basepath}` : ''}`);
}); });
it('export all pages', () => { it('export all pages', () => {
@@ -106,10 +96,7 @@ function testExport({ basepath = '' }) {
'service-worker.js', 'service-worker.js',
'svelte-logo-192.png', 'svelte-logo-192.png',
'svelte-logo-512.png', 'svelte-logo-512.png',
].map(file => { ];
return basepath ? `${basepath.replace(/^[\/\\]/, '')}/${file}` : file;
});
// Client scripts that should show up in the extraction directory. // Client scripts that should show up in the extraction directory.
const expectedClientRegexes = [ const expectedClientRegexes = [
/client\/[^/]+\/main(\.\d+)?\.js/, /client\/[^/]+\/main(\.\d+)?\.js/,
@@ -139,7 +126,7 @@ function testExport({ basepath = '' }) {
}); });
}); });
}); });
} });
function run({ mode, basepath = '' }) { function run({ mode, basepath = '' }) {
describe(`mode=${mode}`, function () { describe(`mode=${mode}`, function () {
@@ -436,33 +423,6 @@ function run({ mode, basepath = '' }) {
}); });
}); });
it('redirects on server (root)', () => {
return nightmare.goto(`${base}/redirect-root`)
.path()
.then(path => {
assert.equal(path, `${basepath}/`);
})
.then(() => nightmare.page.title())
.then(title => {
assert.equal(title, 'Great success!');
});
});
it('redirects in client (root)', () => {
return nightmare.goto(base)
.wait('[href="redirect-root"]')
.click('[href="redirect-root"]')
.wait(200)
.path()
.then(path => {
assert.equal(path, `${basepath}/`);
})
.then(() => nightmare.page.title())
.then(title => {
assert.equal(title, 'Great success!');
});
});
it('handles 4xx error on server', () => { it('handles 4xx error on server', () => {
return nightmare.goto(`${base}/blog/nope`) return nightmare.goto(`${base}/blog/nope`)
.path() .path()
@@ -608,11 +568,11 @@ function run({ mode, basepath = '' }) {
return nightmare.goto(`${base}/store`) return nightmare.goto(`${base}/store`)
.page.title() .page.title()
.then(title => { .then(title => {
assert.equal(title, 'hello world'); assert.equal(title, 'Stored title');
return nightmare.init().page.title(); return nightmare.init().page.title();
}) })
.then(title => { .then(title => {
assert.equal(title, 'hello world'); assert.equal(title, 'Stored title');
}); });
}); });