Compare commits

..

29 Commits

Author SHA1 Message Date
Rich Harris
611017fd28 -> v0.16.0 2018-08-19 17:39:02 -04:00
Rich Harris
72b265a35f Merge pull request #363 from sveltejs/bundle-deps
[WIP] Slim down the bundle
2018-08-19 17:36:18 -04:00
Rich Harris
e0d533f2ea bundle more stuff 2018-08-19 16:15:04 -04:00
Rich Harris
dba83641e4 remove glob and cheerio from dependencies 2018-08-19 15:20:14 -04:00
Rich Harris
14e5c8e761 update lockfile 2018-08-16 12:48:50 -04:00
Rich Harris
cbbf4a95db -> v0.15.8 2018-08-16 12:46:13 -04:00
Rich Harris
55b7ffd2ed Merge pull request #361 from sveltejs/handle-unknown-preload-errors
handle unknown preload errors
2018-08-16 12:44:27 -04:00
Rich Harris
9f4d4e70de can remove this, preloading is set false on render 2018-08-16 12:39:54 -04:00
Rich Harris
deef1bbfcf handle unknown preload errors 2018-08-16 12:25:23 -04:00
Seth Thompson
17b0fc0d0c nit 2018-08-11 17:52:53 -04:00
Seth Thompson
3c44c511e4 make sure page has expected preloading value 2018-08-11 17:51:44 -04:00
Seth Thompson
7cf1b9613a prefetching should not set root preloading value, closes #352 2018-08-11 12:26:27 -04:00
Rich Harris
99e5a9601c -> v0.15.7 2018-08-09 20:14:37 -04:00
Rich Harris
4c9c1dccf5 Merge pull request #350 from sveltejs/gh-344
pass response object to store getter
2018-08-09 20:13:17 -04:00
Rich Harris
2cddd5afa0 Merge pull request #345 from sveltejs/fix/redirect
Fix Preload's Redirect
2018-08-09 20:08:51 -04:00
Rich Harris
8c6a0c4773 Merge branch 'master' into gh-344 2018-08-09 20:03:37 -04:00
Rich Harris
af5063552d Merge pull request #351 from sveltejs/argh-windows
doh
2018-08-09 20:02:51 -04:00
Rich Harris
419d154794 fffffuuuuu 2018-08-09 19:53:26 -04:00
Rich Harris
abda059be5 doh 2018-08-09 19:46:09 -04:00
Rich Harris
444908cac5 pass response object to store getter - fixes #344 2018-08-08 10:57:10 -04:00
Luke Edwards
c6da26e1a0 add redirect test to root (“/“) 2018-08-06 20:29:28 -07:00
Luke Edwards
aad87857ce fix: replace leading slash in preload’s redirect 2018-08-06 20:28:28 -07:00
Rich Harris
666c113297 -> v0.15.6 2018-08-06 22:36:17 -04:00
Rich Harris
84a58f34a0 add test for exporting with custom basepath 2018-08-06 22:35:02 -04:00
Rich Harris
75f5b5c721 Merge pull request #342 from aubergene/gh-338
Remove basepath from deferred urls and add trailing slash to root
2018-08-06 22:06:09 -04:00
Julian Burgess
a176a3b79b Remove basepath from deferred urls and add trailing slash to root request 2018-08-06 16:43:02 +01:00
Rich Harris
1627a5767a -> v0.15.5 2018-08-03 01:18:12 -04:00
Rich Harris
6ff3a9e9ab Merge branch 'master' of github.com:sveltejs/sapper 2018-08-03 01:16:43 -04:00
Rich Harris
3ce2bd30f9 Use npm ci instead of npm install (#336)
* dont write server_info.json either - second half of #318

* use npm ci

* update lockfile

* try this
2018-08-03 01:16:26 -04:00
41 changed files with 1757 additions and 1689 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 install - npm ci || npm i

View File

@@ -1,5 +1,23 @@
# sapper changelog # sapper changelog
## 0.16.0
* Slim down installed package ([#363](https://github.com/sveltejs/sapper/pull/363))
## 0.15.8
* Only set `preloading: true` on navigation, not prefetch ([#352](https://github.com/sveltejs/sapper/issues/352))
* Provide fallback for missing preload errors ([#361](https://github.com/sveltejs/sapper/pull/361))
## 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 install - npm ci
test_script: test_script:
- node --version && npm --version - node --version && npm --version

View File

@@ -1,4 +1,5 @@
--require source-map-support/register --require source-map-support/register
--require ts-node/register
--recursive --recursive
test/unit/*/*.js test/unit/*/*.ts
test/common/test.js test/common/test.js

2778
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "sapper", "name": "sapper",
"version": "0.15.4", "version": "0.16.0",
"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": {
@@ -19,50 +19,53 @@
"test": "test" "test": "test"
}, },
"dependencies": { "dependencies": {
"ansi-colors": "^2.0.1", "chokidar": "^2.0.4",
"cheerio": "^1.0.0-rc.2", "html-minifier": "^3.5.16",
"chokidar": "^2.0.3", "source-map-support": "^0.5.6",
"tslib": "^1.9.1"
},
"devDependencies": {
"@types/glob": "^5.0.34",
"@types/mkdirp": "^0.5.2",
"@types/mocha": "^5.2.5",
"@types/node": "^10.7.1",
"@types/rimraf": "^2.0.2",
"compression": "^1.7.1",
"cookie": "^0.3.1", "cookie": "^0.3.1",
"devalue": "^1.0.4", "devalue": "^1.0.4",
"glob": "^7.1.2", "eslint": "^4.13.1",
"html-minifier": "^3.5.16", "eslint-plugin-import": "^2.12.0",
"express": "^4.16.3",
"kleur": "^2.0.1",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"mocha": "^5.2.0",
"nightmare": "^3.0.0",
"node-fetch": "^2.1.1", "node-fetch": "^2.1.1",
"npm-run-all": "^4.1.3",
"polka": "^0.4.0",
"port-authority": "^1.0.5", "port-authority": "^1.0.5",
"pretty-bytes": "^5.0.0", "pretty-bytes": "^5.0.0",
"pretty-ms": "^3.1.0", "pretty-ms": "^3.1.0",
"require-relative": "^0.8.7", "require-relative": "^0.8.7",
"rimraf": "^2.6.2", "rimraf": "^2.6.2",
"sade": "^1.4.1",
"sander": "^0.6.0",
"source-map-support": "^0.5.6",
"tslib": "^1.9.1",
"url-parse": "^1.2.0",
"webpack-format-messages": "^2.0.1"
},
"devDependencies": {
"@types/glob": "^5.0.34",
"@types/mkdirp": "^0.5.2",
"@types/rimraf": "^2.0.2",
"compression": "^1.7.1",
"eslint": "^4.13.1",
"eslint-plugin-import": "^2.12.0",
"express": "^4.16.3",
"mocha": "^5.2.0",
"nightmare": "^3.0.0",
"npm-run-all": "^4.1.3",
"polka": "^0.4.0",
"rollup": "^0.59.2", "rollup": "^0.59.2",
"rollup-plugin-commonjs": "^9.1.3", "rollup-plugin-commonjs": "^9.1.3",
"rollup-plugin-json": "^3.0.0", "rollup-plugin-json": "^3.0.0",
"rollup-plugin-node-resolve": "^3.3.0",
"rollup-plugin-string": "^2.0.2", "rollup-plugin-string": "^2.0.2",
"rollup-plugin-typescript": "^0.8.1", "rollup-plugin-typescript": "^0.8.1",
"sade": "^1.4.1",
"sander": "^0.6.0",
"serve-static": "^1.13.2", "serve-static": "^1.13.2",
"svelte": "^2.6.3", "svelte": "^2.6.3",
"svelte-loader": "^2.9.0", "svelte-loader": "^2.9.0",
"tiny-glob": "^0.2.2",
"ts-node": "^7.0.1",
"typescript": "^2.8.3", "typescript": "^2.8.3",
"url-parse": "^1.2.0",
"walk-sync": "^0.3.2", "walk-sync": "^0.3.2",
"webpack": "^4.8.3" "webpack": "^4.8.3",
"webpack-format-messages": "^2.0.1"
}, },
"scripts": { "scripts": {
"cy:open": "cypress open", "cy:open": "cypress open",

View File

@@ -1,6 +1,7 @@
import typescript from 'rollup-plugin-typescript'; import typescript from 'rollup-plugin-typescript';
import string from 'rollup-plugin-string'; import string from 'rollup-plugin-string';
import json from 'rollup-plugin-json'; import json from 'rollup-plugin-json';
import resolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs'; import commonjs from 'rollup-plugin-commonjs';
import pkg from './package.json'; import pkg from './package.json';
@@ -44,6 +45,7 @@ export default [
include: '**/*.md' include: '**/*.md'
}), }),
json(), json(),
resolve(),
commonjs(), commonjs(),
typescript({ typescript({
typescript: require('typescript') typescript: require('typescript')

View File

@@ -3,7 +3,7 @@ import * as path from 'path';
import mkdirp from 'mkdirp'; import mkdirp from 'mkdirp';
import rimraf from 'rimraf'; import rimraf from 'rimraf';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { minify_html } from './utils/minify_html'; import minify_html from './utils/minify_html';
import { create_compilers, create_main_manifests, create_routes, create_serviceworker_manifest } from '../core'; import { create_compilers, create_main_manifests, create_routes, create_serviceworker_manifest } from '../core';
import * as events from './interfaces'; import * as events from './interfaces';

View File

@@ -224,8 +224,11 @@ class Watcher extends EventEmitter {
}); });
this.proc.on('message', message => { this.proc.on('message', message => {
if (!message.__sapper__) return; if (message.__sapper__ && message.event === 'basepath') {
this.emit(message.event, message); this.emit('basepath', {
basepath: message.basepath
});
}
}); });
this.proc.on('exit', emitFatal); this.proc.on('exit', emitFatal);
@@ -451,19 +454,23 @@ class DevServer {
function noop() {} function noop() {}
function watch_files(pattern: string, events: string[], callback: () => void) { function watch_files(pattern: string, events: string[], callback: () => void) {
const chokidar = require('chokidar'); let watcher;
const watcher = chokidar.watch(pattern, { import('chokidar').then(({ default: chokidar }) => {
persistent: true, if (closed) return;
ignoreInitial: true,
disableGlobbing: true
});
events.forEach(event => { watcher = chokidar.watch(pattern, {
watcher.on(event, callback); persistent: true,
ignoreInitial: true,
disableGlobbing: true
});
events.forEach(event => {
watcher.on(event, callback);
});
}); });
return { return {
close: () => watcher.close() close: () => watcher && watcher.close()
}; };
} }

View File

@@ -1,12 +1,12 @@
import * as child_process from 'child_process'; import * as child_process from 'child_process';
import * as path from 'path'; import * as path from 'path';
import * as sander from 'sander'; import * as sander from 'sander';
import cheerio from 'cheerio';
import URL from 'url-parse'; import URL from 'url-parse';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import * as ports from 'port-authority'; import * as ports from 'port-authority';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { minify_html } from './utils/minify_html'; import clean_html from './utils/clean_html';
import minify_html from './utils/minify_html';
import Deferred from './utils/Deferred'; import Deferred from './utils/Deferred';
import * as events from './interfaces'; import * as events from './interfaces';
@@ -51,9 +51,10 @@ 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 ${origin}` message: `Crawling ${root.href}`
}); });
const proc = child_process.fork(path.resolve(`${build}/server.js`), [], { const proc = child_process.fork(path.resolve(`${build}/server.js`), [], {
@@ -71,8 +72,10 @@ 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());
} }
return deferreds.get(pathname); return deferreds.get(pathname);
@@ -107,7 +110,7 @@ async function execute(emitter: EventEmitter, {
}); });
async function handle(url: URL) { async function handle(url: URL) {
const pathname = url.pathname || '/'; const pathname = (url.pathname.replace(root.pathname, '') || '/');
if (seen.has(pathname)) return; if (seen.has(pathname)) return;
seen.add(pathname); seen.add(pathname);
@@ -120,15 +123,26 @@ async function execute(emitter: EventEmitter, {
if (range === 2) { if (range === 2) {
if (r.headers.get('Content-Type') === 'text/html') { if (r.headers.get('Content-Type') === 'text/html') {
const body = await r.text(); const body = await r.text();
const $ = cheerio.load(body);
const urls: URL[] = []; const urls: URL[] = [];
const base = new URL($('base').attr('href') || '/', url.href); const cleaned = clean_html(body);
$('a[href]').each((i: number, $a) => { const base_match = /<base ([\s\S]+?)>/m.exec(cleaned);
const url = new URL($a.attribs.href, base.href); const base_href = base_match && get_href(base_match[1]);
if (url.origin === origin) urls.push(url); const base = new URL(base_href || '/', url.href);
});
let match;
let pattern = /<a ([\s\S]+?)>/gm;
while (match = pattern.exec(cleaned)) {
const attrs = match[1];
const href = get_href(attrs);
if (href) {
const url = new URL(href, base.href);
if (url.origin === origin) urls.push(url);
}
}
await Promise.all(urls.map(handle)); await Promise.all(urls.map(handle));
} }
@@ -138,6 +152,14 @@ async function execute(emitter: EventEmitter, {
} }
return ports.wait(port) return ports.wait(port)
.then(() => handle(new URL(`/${basepath}`, origin))) // TODO all static routes .then(() => {
// TODO all static routes
return handle(root);
})
.then(() => proc.kill()); .then(() => proc.kill());
} }
function get_href(attrs: string) {
const match = /href\s*=\s*(?:"(.+?)"|'(.+?)'|([^\s>]+))/.exec(attrs);
return match[1] || match[2] || match[3];
}

View File

@@ -1,4 +1,3 @@
import * as glob from 'glob';
import { locations } from '../config'; import { locations } from '../config';
import { create_routes } from '../core'; import { create_routes } from '../core';

View File

@@ -0,0 +1,7 @@
export default function clean_html(html: string) {
return html
.replace(/<!\[CDATA\[[\s\S]*?\]\]>/gm, '')
.replace(/(<script[\s\S]*?>)[\s\S]*?<\/script>/gm, '$1</' + 'script>')
.replace(/(<style[\s\S]*?>)[\s\S]*?<\/style>/gm, '$1</' + 'style>')
.replace(/<!--[\s\S]*?-->/gm, '');
}

View File

@@ -1,6 +1,6 @@
import { minify } from 'html-minifier'; import { minify } from 'html-minifier';
export function minify_html(html: string) { export default function minify_html(html: string) {
return minify(html, { return minify(html, {
collapseBooleanAttributes: true, collapseBooleanAttributes: true,
collapseWhitespace: true, collapseWhitespace: true,

View File

@@ -1,7 +1,7 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import sade from 'sade'; import sade from 'sade';
import * as colors from 'ansi-colors'; import colors from 'kleur';
import prettyMs from 'pretty-ms'; import prettyMs from 'pretty-ms';
import * as pkg from '../package.json'; import * as pkg from '../package.json';

View File

@@ -1,5 +1,5 @@
import { build as _build } from '../api/build'; import { build as _build } from '../api/build';
import * as colors from 'ansi-colors'; import colors from 'kleur';
import { locations } from '../config'; import { locations } from '../config';
export function build() { export function build() {

View File

@@ -1,8 +1,7 @@
import * as path from 'path'; import * as path from 'path';
import * as colors from 'ansi-colors'; import colors from 'kleur';
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';
@@ -45,32 +44,6 @@ 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

@@ -1,5 +1,5 @@
import { exporter as _exporter } from '../api/export'; import { exporter as _exporter } from '../api/export';
import * as colors from 'ansi-colors'; import colors from 'kleur';
import prettyBytes from 'pretty-bytes'; import prettyBytes from 'pretty-bytes';
import { locations } from '../config'; import { locations } from '../config';

View File

@@ -1,7 +1,7 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as child_process from 'child_process'; import * as child_process from 'child_process';
import * as colors from 'ansi-colors'; import colors from 'kleur';
import * as ports from 'port-authority'; import * as ports from 'port-authority';
export async function start(dir: string, opts: { port: number, open: boolean }) { export async function start(dir: string, opts: { port: number, open: boolean }) {

View File

@@ -1,5 +1,5 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as colors from 'ansi-colors'; import colors from 'kleur';
export default async function upgrade() { export default async function upgrade() {
const upgraded = [ const upgraded = [

View File

@@ -1,6 +1,6 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as glob from 'glob'; import glob from 'tiny-glob/sync.js';
import { posixify, write_if_changed } from './utils'; import { posixify, write_if_changed } from './utils';
import { dev, locations } from '../config'; import { dev, locations } from '../config';
import { Page, PageComponent, ServerRoute } from '../interfaces'; import { Page, PageComponent, ServerRoute } from '../interfaces';
@@ -9,24 +9,27 @@ export function create_main_manifests({ routes, dev_port }: {
routes: { components: PageComponent[], pages: Page[], server_routes: ServerRoute[] }; routes: { components: PageComponent[], pages: Page[], server_routes: ServerRoute[] };
dev_port?: number; dev_port?: number;
}) { }) {
const path_to_routes = path.relative(`${locations.app()}/manifest`, locations.routes()); const manifest_dir = path.join(locations.app(), 'manifest');
if (!fs.existsSync(manifest_dir)) fs.mkdirSync(manifest_dir);
const path_to_routes = path.relative(manifest_dir, locations.routes());
const client_manifest = generate_client(routes, path_to_routes, dev_port); const client_manifest = generate_client(routes, path_to_routes, dev_port);
const server_manifest = generate_server(routes, path_to_routes); const server_manifest = generate_server(routes, path_to_routes);
write_if_changed( write_if_changed(
`${locations.app()}/manifest/default-layout.html`, `${manifest_dir}/default-layout.html`,
`<svelte:component this={child.component} {...child.props}/>` `<svelte:component this={child.component} {...child.props}/>`
); );
write_if_changed(`${locations.app()}/manifest/client.js`, client_manifest); write_if_changed(`${manifest_dir}/client.js`, client_manifest);
write_if_changed(`${locations.app()}/manifest/server.js`, server_manifest); write_if_changed(`${manifest_dir}/server.js`, server_manifest);
} }
export function create_serviceworker_manifest({ routes, client_files }: { export function create_serviceworker_manifest({ routes, client_files }: {
routes: { components: PageComponent[], pages: Page[], server_routes: ServerRoute[] }; routes: { components: PageComponent[], pages: Page[], server_routes: ServerRoute[] };
client_files: string[]; client_files: string[];
}) { }) {
const assets = glob.sync('**', { cwd: 'assets', nodir: true }); const assets = glob('**', { cwd: 'assets', filesOnly: true });
let code = ` let code = `
// This file is generated by Sapper — do not edit it! // This file is generated by Sapper — do not edit it!
@@ -42,11 +45,6 @@ export function create_serviceworker_manifest({ routes, client_files }: {
write_if_changed(`${locations.app()}/manifest/service-worker.js`, code); write_if_changed(`${locations.app()}/manifest/service-worker.js`, code);
} }
function right_pad(str: string, len: number) {
while (str.length < len) str += ' ';
return str;
}
function generate_client( function generate_client(
routes: { root: PageComponent, components: PageComponent[], pages: Page[], server_routes: ServerRoute[] }, routes: { root: PageComponent, components: PageComponent[], pages: Page[], server_routes: ServerRoute[] },
path_to_routes: string, path_to_routes: string,
@@ -156,7 +154,6 @@ 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

@@ -4,11 +4,6 @@ import { locations } from '../config';
import { Page, PageComponent, ServerRoute } from '../interfaces'; import { Page, PageComponent, ServerRoute } from '../interfaces';
import { posixify } from './utils'; import { posixify } from './utils';
const default_layout_file = posixify(path.resolve(
__dirname,
'../components/default-layout.html'
));
export default function create_routes(cwd = locations.routes()) { export default function create_routes(cwd = locations.routes()) {
const components: PageComponent[] = []; const components: PageComponent[] = [];
const pages: Page[] = []; const pages: Page[] = [];

View File

@@ -1,11 +1,11 @@
import * as sander from 'sander'; import * as fs from 'fs';
const previous_contents = new Map(); const previous_contents = new Map();
export function write_if_changed(file: string, code: string) { export function write_if_changed(file: string, code: string) {
if (code !== previous_contents.get(file)) { if (code !== previous_contents.get(file)) {
previous_contents.set(file, code); previous_contents.set(file, code);
sander.writeFileSync(file, code); fs.writeFileSync(file, code);
fudge_mtime(file); fudge_mtime(file);
} }
} }
@@ -16,8 +16,8 @@ export function posixify(file: string) {
export function fudge_mtime(file: string) { export function fudge_mtime(file: string) {
// need to fudge the mtime so that webpack doesn't go doolally // need to fudge the mtime so that webpack doesn't go doolally
const { atime, mtime } = sander.statSync(file); const { atime, mtime } = fs.statSync(file);
sander.utimesSync( fs.utimesSync(
file, file,
new Date(atime.getTime() - 999999), new Date(atime.getTime() - 999999),
new Date(mtime.getTime() - 999999) new Date(mtime.getTime() - 999999)

View File

@@ -8,9 +8,6 @@ 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();
@@ -86,7 +83,7 @@ function toIgnore(uri: string, val: any) {
export default function middleware(opts: { export default function middleware(opts: {
manifest: Manifest, manifest: Manifest,
store: (req: Req) => Store, store: (req: Req, res: ServerResponse) => Store,
ignore?: any, ignore?: any,
routes?: any // legacy routes?: any // legacy
}) { }) {
@@ -279,7 +276,10 @@ function get_server_route_handler(routes: ServerRoute[]) {
}; };
} }
function get_page_handler(manifest: Manifest, store_getter: (req: Req) => Store) { function get_page_handler(
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,21 +293,16 @@ function get_page_handler(manifest: Manifest, store_getter: (req: Req) => Store)
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,
parts: [ parts: [
{ name: null, component: error_route } { name: null, component: error_route }
] ]
}, req, res, statusCode, error); }, req, res, statusCode, error || new Error('Unknown error in preload function'));
} }
function handle_page(page: Page, req: Req, res: ServerResponse, status = 200, error: Error | string = null) { function handle_page(page: Page, req: Req, res: ServerResponse, status = 200, error: Error | string = null) {
const get_params = page.parts[page.parts.length - 1].params || (() => ({}));
const match = error ? null : page.pattern.exec(req.path);
const chunks: Record<string, string | string[]> = get_chunks(); const chunks: Record<string, string | string[]> = get_chunks();
res.setHeader('Content-Type', 'text/html'); res.setHeader('Content-Type', 'text/html');
@@ -331,7 +326,7 @@ function get_page_handler(manifest: Manifest, store_getter: (req: Req) => Store)
res.setHeader('Link', link); res.setHeader('Link', link);
const store = store_getter ? store_getter(req) : null; const store = store_getter ? store_getter(req, res) : 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 };
@@ -341,6 +336,7 @@ function get_page_handler(manifest: Manifest, store_getter: (req: Req) => Store)
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) => {
@@ -392,6 +388,8 @@ function get_page_handler(manifest: Manifest, store_getter: (req: Req) => Store)
}) })
: {}; : {};
const match = error ? null : page.pattern.exec(req.path);
Promise.all([root_preloaded].concat(page.parts.map(part => { Promise.all([root_preloaded].concat(page.parts.map(part => {
if (!part) return null; if (!part) return null;
@@ -433,6 +431,11 @@ function get_page_handler(manifest: Manifest, store_getter: (req: Req) => Store)
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 = {
@@ -454,15 +457,6 @@ function get_page_handler(manifest: Manifest, store_getter: (req: Req) => Store)
} }
}); });
// 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];
@@ -474,7 +468,7 @@ function get_page_handler(manifest: Manifest, store_getter: (req: Req) => Store)
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"]>{
@@ -493,47 +487,6 @@ function get_page_handler(manifest: Manifest, store_getter: (req: Req) => Store)
.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}"`,
@@ -557,13 +510,6 @@ function get_page_handler(manifest: Manifest, store_getter: (req: Req) => Store)
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',
@@ -639,4 +585,4 @@ function escape_html(html: string) {
}; };
return html.replace(/["'&<>]/g, c => `&${chars[c]};`); return html.replace(/["'&<>]/g, c => `&${chars[c]};`);
} }

View File

@@ -1,34 +0,0 @@
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

@@ -1,85 +0,0 @@
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

@@ -136,10 +136,6 @@ function prepare_page(target: Target): Promise<{
data?: any; data?: any;
nullable_depth?: number; nullable_depth?: number;
}> { }> {
if (root) {
root.set({ preloading: true });
}
const { page, path, query } = target; const { page, path, query } = target;
const new_segments = path.split('/').filter(Boolean); const new_segments = path.split('/').filter(Boolean);
let changed_from = 0; let changed_from = 0;
@@ -285,6 +281,9 @@ async function navigate(target: Target, id: number): Promise<any> {
cid = id; cid = id;
if (root) {
root.set({ preloading: true });
}
const loaded = prefetching && prefetching.href === target.url.href ? const loaded = prefetching && prefetching.href === target.url.href ?
prefetching.promise : prefetching.promise :
prepare_page(target); prepare_page(target);

View File

@@ -85,11 +85,18 @@ 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: () => { store: (req, res) => {
return new Store({ return new Store({
title: 'Stored title' title: `${req.hello} ${res.locals.name}`
}); });
}, },
ignore: [ ignore: [

View File

@@ -8,6 +8,7 @@
<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

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

View File

@@ -59,9 +59,19 @@ describe('sapper', function() {
basepath: '/custom-basepath' basepath: '/custom-basepath'
}); });
describe('export', () => { testExport({});
testExport({ basepath: '/custom-basepath' });
});
function testExport({ basepath = '' }) {
describe(basepath ? `export --basepath ${basepath}` : 'export', () => {
before(() => { before(() => {
return exec(`node ${cli} export`); if (basepath) {
process.env.BASEPATH = basepath;
}
return exec(`node ${cli} export ${basepath ? `--basepath ${basepath}` : ''}`);
}); });
it('export all pages', () => { it('export all pages', () => {
@@ -96,7 +106,10 @@ describe('sapper', function() {
'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/,
@@ -126,7 +139,7 @@ describe('sapper', function() {
}); });
}); });
}); });
}); }
function run({ mode, basepath = '' }) { function run({ mode, basepath = '' }) {
describe(`mode=${mode}`, function () { describe(`mode=${mode}`, function () {
@@ -423,6 +436,33 @@ 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()
@@ -568,11 +608,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, 'Stored title'); assert.equal(title, 'hello world');
return nightmare.init().page.title(); return nightmare.init().page.title();
}) })
.then(title => { .then(title => {
assert.equal(title, 'Stored title'); assert.equal(title, 'hello world');
}); });
}); });

View File

@@ -0,0 +1,26 @@
import * as fs from 'fs';
import * as path from 'path';
import * as assert from 'assert';
import clean_html from '../../../src/api/utils/clean_html';
describe('clean_html', () => {
const samples = path.join(__dirname, 'samples');
fs.readdirSync(samples).forEach(dir => {
if (dir[0] === '.') return;
it(dir, () => {
const input = fs.readFileSync(`${samples}/${dir}/input.html`, 'utf-8');
const expected = fs.readFileSync(`${samples}/${dir}/output.html`, 'utf-8');
const actual = clean_html(input);
fs.writeFileSync(`${samples}/${dir}/.actual.html`, actual);
assert.equal(
actual.replace(/\s+$/gm, ''),
expected.replace(/\s+$/gm, '')
);
});
});
});

View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
</head>
<body>
<math>
<ms></ms>
<mo>+</mo>
<mn>3</mn>
<mo>=</mo>
<ms></ms>
</math>
</body>
</html>

View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
</head>
<body>
<math>
<ms><![CDATA[x<y]]></ms>
<mo>+</mo>
<mn>3</mn>
<mo>=</mo>
<ms><![CDATA[x<y3]]></ms>
</math>
</body>
</html>

View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
</head>
<body>
<math>
<ms></ms>
<mo>+</mo>
<mn>3</mn>
<mo>=</mo>
<ms></ms>
</math>
</body>
</html>

View File

@@ -0,0 +1,9 @@
<!doctype html>
<html lang="en">
<head>
</head>
<body>
<a href="keep-me">keep me</a>
</body>
</html>

View File

@@ -0,0 +1,9 @@
<!doctype html>
<html lang="en">
<head>
</head>
<body>
<a href="keep-me">keep me</a>
<!-- <a href="delete-me">delete me</a> -->
</body>
</html>

View File

@@ -0,0 +1,9 @@
<!doctype html>
<html lang="en">
<head>
</head>
<body>
<a href="keep-me">keep me</a>
</body>
</html>

View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
</head>
<body>
<script></script>
<script></script>
<script src="attributes-are-preserved.js"></script>
</body>
</html>

View File

@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
</head>
<body>
<script>
console.log('this should be deleted');
</script>
<script>
console.log('so should this');
</script>
<script src="attributes-are-preserved.js"></script>
</body>
</html>

View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
</head>
<body>
<script></script>
<script></script>
<script src="attributes-are-preserved.js"></script>
</body>
</html>

View File

@@ -1,6 +1,6 @@
const path = require('path'); import * as path from 'path';
const assert = require('assert'); import * as assert from 'assert';
const { create_routes } = require('../../../dist/core.ts.js'); import create_routes from '../../../src/core/create_routes';
describe('create_routes', () => { describe('create_routes', () => {
it('creates routes', () => { it('creates routes', () => {