diff --git a/.gitignore b/.gitignore
index 57aee3f..8ac5504 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,8 +7,7 @@ test/app/.sapper
test/app/app/manifest
test/app/export
test/app/build
-*.js
-*.js.map
-*.ts.js
-*.ts.js.map
+sapper
+runtime.js
+dist
!rollup.config.js
\ No newline at end of file
diff --git a/package.json b/package.json
index 1550578..37a0b75 100644
--- a/package.json
+++ b/package.json
@@ -46,6 +46,7 @@
"compression": "^1.7.1",
"eslint": "^4.13.1",
"eslint-plugin-import": "^2.8.0",
+ "express": "^4.16.3",
"get-port": "^3.2.0",
"mocha": "^5.0.4",
"nightmare": "^3.0.0",
diff --git a/runtime.js b/runtime.js
deleted file mode 100644
index a3db060..0000000
--- a/runtime.js
+++ /dev/null
@@ -1,271 +0,0 @@
-function detach(node) {
- node.parentNode.removeChild(node);
-}
-function findAnchor(node) {
- while (node && node.nodeName.toUpperCase() !== 'A')
- node = node.parentNode; // SVG elements have a lowercase name
- return node;
-}
-function which(event) {
- return event.which === null ? event.button : event.which;
-}
-function scroll_state() {
- return {
- x: window.scrollX,
- y: window.scrollY
- };
-}
-
-var component;
-var target;
-var routes;
-var errors;
-var history = typeof window !== 'undefined' ? window.history : {
- pushState: function (state, title, href) { },
- replaceState: function (state, title, href) { },
- scrollRestoration: ''
-};
-var scroll_history = {};
-var uid = 1;
-var cid;
-if ('scrollRestoration' in history) {
- history.scrollRestoration = 'manual';
-}
-function select_route(url) {
- if (url.origin !== window.location.origin)
- return null;
- var _loop_1 = function (route) {
- var match = route.pattern.exec(url.pathname);
- if (match) {
- if (route.ignore)
- return { value: null };
- var params = route.params(match);
- var query_1 = {};
- if (url.search.length > 0) {
- url.search.slice(1).split('&').forEach(function (searchParam) {
- var _a = /([^=]+)=(.*)/.exec(searchParam), key = _a[1], value = _a[2];
- query_1[key] = value || true;
- });
- }
- return { value: { url: url, route: route, data: { params: params, query: query_1 } } };
- }
- };
- for (var _i = 0, routes_1 = routes; _i < routes_1.length; _i++) {
- var route = routes_1[_i];
- var state_1 = _loop_1(route);
- if (typeof state_1 === "object")
- return state_1.value;
- }
-}
-var current_token;
-function render(Component, data, scroll, token) {
- if (current_token !== token)
- return;
- if (component) {
- component.destroy();
- }
- else {
- // first load — remove SSR'd contents
- var start = document.querySelector('#sapper-head-start');
- var end = document.querySelector('#sapper-head-end');
- if (start && end) {
- while (start.nextSibling !== end)
- detach(start.nextSibling);
- detach(start);
- detach(end);
- }
- }
- component = new Component({
- target: target,
- data: data,
- hydrate: !component
- });
- if (scroll) {
- window.scrollTo(scroll.x, scroll.y);
- }
-}
-function prepare_route(Component, data) {
- var redirect = null;
- var error = null;
- if (!Component.preload) {
- return { Component: Component, data: data, redirect: redirect, error: error };
- }
- if (!component && window.__SAPPER__ && window.__SAPPER__.preloaded) {
- return { Component: Component, data: Object.assign(data, window.__SAPPER__.preloaded), redirect: redirect, error: error };
- }
- return Promise.resolve(Component.preload.call({
- redirect: function (statusCode, location) {
- redirect = { statusCode: statusCode, location: location };
- },
- error: function (statusCode, message) {
- error = { statusCode: statusCode, message: message };
- }
- }, data))["catch"](function (err) {
- error = { statusCode: 500, message: err };
- }).then(function (preloaded) {
- if (error) {
- var route = error.statusCode >= 400 && error.statusCode < 500
- ? errors['4xx']
- : errors['5xx'];
- return route.load().then(function (_a) {
- var Component = _a["default"];
- var err = error.message instanceof Error ? error.message : new Error(error.message);
- Object.assign(data, { status: error.statusCode, error: err });
- return { Component: Component, data: data, redirect: null };
- });
- }
- Object.assign(data, preloaded);
- return { Component: Component, data: data, redirect: redirect };
- });
-}
-function navigate(target, id) {
- if (id) {
- // popstate or initial navigation
- cid = id;
- }
- else {
- // clicked on a link. preserve scroll state
- scroll_history[cid] = scroll_state();
- id = cid = ++uid;
- scroll_history[cid] = { x: 0, y: 0 };
- }
- cid = id;
- var loaded = prefetching && prefetching.href === target.url.href ?
- prefetching.promise :
- target.route.load().then(function (mod) { return prepare_route(mod["default"], target.data); });
- prefetching = null;
- var token = current_token = {};
- return loaded.then(function (_a) {
- var Component = _a.Component, data = _a.data, redirect = _a.redirect;
- if (redirect) {
- return goto(redirect.location, { replaceState: true });
- }
- render(Component, data, scroll_history[id], token);
- });
-}
-function handle_click(event) {
- // Adapted from https://github.com/visionmedia/page.js
- // MIT license https://github.com/visionmedia/page.js#license
- if (which(event) !== 1)
- return;
- if (event.metaKey || event.ctrlKey || event.shiftKey)
- return;
- if (event.defaultPrevented)
- return;
- var a = findAnchor(event.target);
- if (!a)
- return;
- // check if link is inside an svg
- // in this case, both href and target are always inside an object
- var svg = typeof a.href === 'object' && a.href.constructor.name === 'SVGAnimatedString';
- var href = String(svg ? a.href.baseVal : a.href);
- if (href === window.location.href) {
- event.preventDefault();
- return;
- }
- // Ignore if tag has
- // 1. 'download' attribute
- // 2. rel='external' attribute
- if (a.hasAttribute('download') || a.getAttribute('rel') === 'external')
- return;
- // Ignore if has a target
- if (svg ? a.target.baseVal : a.target)
- return;
- var url = new URL(href);
- // Don't handle hash changes
- if (url.pathname === window.location.pathname && url.search === window.location.search)
- return;
- var target = select_route(url);
- if (target) {
- navigate(target, null);
- event.preventDefault();
- history.pushState({ id: cid }, '', url.href);
- }
-}
-function handle_popstate(event) {
- scroll_history[cid] = scroll_state();
- if (event.state) {
- var url = new URL(window.location.href);
- var target_1 = select_route(url);
- navigate(target_1, event.state.id);
- }
- else {
- // hashchange
- cid = ++uid;
- history.replaceState({ id: cid }, '', window.location.href);
- }
-}
-var prefetching = null;
-function prefetch(href) {
- var selected = select_route(new URL(href));
- if (selected) {
- prefetching = {
- href: href,
- promise: selected.route.load().then(function (mod) { return prepare_route(mod["default"], selected.data); })
- };
- }
-}
-function handle_touchstart_mouseover(event) {
- var a = findAnchor(event.target);
- if (!a || a.rel !== 'prefetch')
- return;
- prefetch(a.href);
-}
-var inited;
-function init(_target, _routes) {
- target = _target;
- routes = _routes.filter(function (r) { return !r.error; });
- errors = {
- '4xx': _routes.find(function (r) { return r.error === '4xx'; }),
- '5xx': _routes.find(function (r) { return r.error === '5xx'; })
- };
- if (!inited) {
- window.addEventListener('click', handle_click);
- window.addEventListener('popstate', handle_popstate);
- // prefetch
- window.addEventListener('touchstart', handle_touchstart_mouseover);
- window.addEventListener('mouseover', handle_touchstart_mouseover);
- inited = true;
- }
- return Promise.resolve().then(function () {
- var _a = window.location, hash = _a.hash, href = _a.href;
- var deep_linked = hash && document.getElementById(hash.slice(1));
- scroll_history[uid] = deep_linked ?
- { x: 0, y: deep_linked.getBoundingClientRect().top } :
- scroll_state();
- history.replaceState({ id: uid }, '', href);
- var target = select_route(new URL(window.location.href));
- return navigate(target, uid);
- });
-}
-function goto(href, opts) {
- if (opts === void 0) { opts = { replaceState: false }; }
- var target = select_route(new URL(href, window.location.href));
- if (target) {
- navigate(target, null);
- if (history)
- history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
- }
- else {
- window.location.href = href;
- }
-}
-function prefetchRoutes(pathnames) {
- if (!routes)
- throw new Error("You must call init() first");
- return routes
- .filter(function (route) {
- if (!pathnames)
- return true;
- return pathnames.some(function (pathname) {
- return route.error
- ? route.error === pathname
- : route.pattern.test(pathname);
- });
- })
- .reduce(function (promise, route) {
- return promise.then(route.load);
- }, Promise.resolve());
-}
-
-export { component, prefetch, init, goto, prefetchRoutes, prefetchRoutes as preloadRoutes };
diff --git a/src/cli.ts b/src/cli.ts
index 4be699c..8913b9f 100755
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -49,7 +49,8 @@ prog.command('start [dir]')
prog.command('export [dest]')
.describe('Export your app as static files (if possible)')
- .action(async (dest = 'export') => {
+ .option('--basepath', 'Specify a base path')
+ .action(async (dest = 'export', opts: { basepath?: string }) => {
console.log(`> Building...`);
process.env.NODE_ENV = 'production';
@@ -63,7 +64,7 @@ prog.command('export [dest]')
console.error(`\n> Built in ${elapsed(start)}. Crawling site...`);
const { exporter } = await import('./cli/export');
- await exporter(dest);
+ await exporter(dest, opts);
console.error(`\n> Finished in ${elapsed(start)}. Type ${clorox.bold.cyan(`npx serve ${dest}`)} to run the app.`);
} catch (err) {
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
diff --git a/src/cli/build.ts b/src/cli/build.ts
index 027cbac..4bafa49 100644
--- a/src/cli/build.ts
+++ b/src/cli/build.ts
@@ -34,7 +34,7 @@ export async function build() {
if (serviceworker) {
create_serviceworker_manifest({
routes,
- client_files: client_stats.toJson().assets.map((chunk: { name: string }) => `/client/${chunk.name}`)
+ client_files: client_stats.toJson().assets.map((chunk: { name: string }) => `client/${chunk.name}`)
});
serviceworker_stats = await compile(serviceworker);
diff --git a/src/cli/dev.ts b/src/cli/dev.ts
index 9ca6f63..8b358e3 100644
--- a/src/cli/dev.ts
+++ b/src/cli/dev.ts
@@ -259,7 +259,7 @@ export async function dev(opts: { port: number, open: boolean }) {
fs.writeFileSync(path.join(dir, 'client_info.json'), JSON.stringify(info, null, ' '));
deferreds.client.fulfil();
- const client_files = info.assets.map((chunk: { name: string }) => `/client/${chunk.name}`);
+ const client_files = info.assets.map((chunk: { name: string }) => `client/${chunk.name}`);
deferreds.server.promise.then(() => {
hot_update_server.send({
diff --git a/src/cli/export.ts b/src/cli/export.ts
index 38e466f..95a155d 100644
--- a/src/cli/export.ts
+++ b/src/cli/export.ts
@@ -10,9 +10,11 @@ import prettyBytes from 'pretty-bytes';
import { minify_html } from './utils/minify_html';
import { locations } from '../config';
-export async function exporter(export_dir: string) {
+export async function exporter(export_dir: string, { basepath = '' }) {
const build_dir = locations.dest();
+ export_dir = path.join(export_dir, basepath);
+
// Prep output directory
sander.rimrafSync(export_dir);
@@ -58,38 +60,43 @@ export async function exporter(export_dir: string) {
console.log(`${clorox.bold.cyan(file)} ${clorox.gray(`(${prettyBytes(body.length)})`)}`);
- sander.writeFileSync(`${export_dir}/${file}`, body);
+ sander.writeFileSync(export_dir, file, body);
});
- function handle(url: URL) {
- if (url.origin !== origin) return;
+ async function handle(url: URL) {
+ const r = await fetch(url.href);
+ const range = ~~(r.status / 100);
- if (seen.has(url.pathname)) return;
- seen.add(url.pathname);
+ if (range >= 4) {
+ console.log(`${clorox.red(`> Received ${r.status} response when fetching ${url.pathname}`)}`);
+ return;
+ }
- return fetch(url.href)
- .then(r => {
- if (r.headers.get('Content-Type') === 'text/html') {
- return r.text().then((body: string) => {
- const $ = cheerio.load(body);
- const hrefs: string[] = [];
+ if (range === 2) {
+ if (r.headers.get('Content-Type') === 'text/html') {
+ const body = await r.text();
+ const $ = cheerio.load(body);
+ const urls: URL[] = [];
- $('a[href]').each((i: number, $a) => {
- hrefs.push($a.attribs.href);
- });
+ const base = new URL($('base').attr('href') || '/', url.href);
- return hrefs.reduce((promise, href) => {
- return promise.then(() => handle(new URL(href, url.href)));
- }, Promise.resolve());
- });
+ $('a[href]').each((i: number, $a) => {
+ const url = new URL($a.attribs.href, base.href);
+
+ if (url.origin === origin && !seen.has(url.pathname)) {
+ seen.add(url.pathname);
+ urls.push(url);
+ }
+ });
+
+ for (const url of urls) {
+ await handle(url);
}
- })
- .catch((err: Error) => {
- console.log(`${clorox.red(`> Error rendering ${url.pathname}: ${err.message}`)}`);
- });
+ }
+ }
}
return ports.wait(port)
- .then(() => handle(new URL(origin))) // TODO all static routes
+ .then(() => handle(new URL(`/${basepath}`, origin))) // TODO all static routes
.then(() => proc.kill());
}
\ No newline at end of file
diff --git a/src/middleware.ts b/src/middleware.ts
index bc23a35..cdfde82 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -1,5 +1,6 @@
import * as fs from 'fs';
import * as path from 'path';
+import { resolve } from 'url';
import { ClientRequest, ServerResponse } from 'http';
import mkdirp from 'mkdirp';
import rimraf from 'rimraf';
@@ -46,7 +47,16 @@ export default function middleware({ routes }: {
const middleware = compose_handlers([
(req: Req, res: ServerResponse, next: () => void) => {
- req.pathname = req.url.replace(/\?.*/, '');
+ if (req.baseUrl === undefined) {
+ req.baseUrl = req.originalUrl
+ ? req.originalUrl.slice(0, -req.url.length)
+ : '';
+ }
+
+ if (req.path === undefined) {
+ req.path = req.url.replace(/\?.*/, '');
+ }
+
next();
},
@@ -77,8 +87,8 @@ function serve({ prefix, pathname, cache_control }: {
cache_control: string
}) {
const filter = pathname
- ? (req: Req) => req.pathname === pathname
- : (req: Req) => req.pathname.startsWith(prefix);
+ ? (req: Req) => req.path === pathname
+ : (req: Req) => req.path.startsWith(prefix);
const output = locations.dest();
@@ -90,10 +100,10 @@ function serve({ prefix, pathname, cache_control }: {
return (req: Req, res: ServerResponse, next: () => void) => {
if (filter(req)) {
- const type = lookup(req.pathname);
+ const type = lookup(req.path);
try {
- const data = read(req.pathname.slice(1));
+ const data = read(req.path.slice(1));
res.setHeader('Content-Type', type);
res.setHeader('Cache-Control', cache_control);
@@ -116,7 +126,7 @@ function get_route_handler(chunks: Record, routes: RouteObject[]
: (str => () => str)(fs.readFileSync(`${locations.dest()}/template.html`, 'utf-8'));
function handle_route(route: RouteObject, req: Req, res: ServerResponse) {
- req.params = route.params(route.pattern.exec(req.pathname));
+ req.params = route.params(route.pattern.exec(req.path));
const mod = route.module;
@@ -127,7 +137,7 @@ function get_route_handler(chunks: Record, routes: RouteObject[]
// TODO detect other stuff we can preload? images, CSS, fonts?
const link = []
.concat(chunks.main, chunks[route.id])
- .map(file => `;rel="preload";as="script"`)
+ .map(file => `<${req.baseUrl}/client/${file}>;rel="preload";as="script"`)
.join(', ');
res.setHeader('Link', link);
@@ -151,7 +161,7 @@ function get_route_handler(chunks: Record, routes: RouteObject[]
}).then(preloaded => {
if (redirect) {
res.statusCode = redirect.statusCode;
- res.setHeader('Location', redirect.location);
+ res.setHeader('Location', `${req.baseUrl}/${redirect.location}`);
res.end();
return;
@@ -169,13 +179,22 @@ function get_route_handler(chunks: Record, routes: RouteObject[]
let scripts = []
.concat(chunks.main) // chunks main might be an array. it might not! thanks, webpack
- .map(file => ``)
+ .map(file => ``)
.join('');
- scripts = `${scripts}`;
+ let inline_script = `__SAPPER__={${[
+ `baseUrl: "${req.baseUrl}"`,
+ mod.preload && serialized && `preloaded: ${serialized}`,
+ ].filter(Boolean).join(',')}}`
+
+ const has_service_worker = fs.existsSync(path.join(locations.dest(), 'service-worker.js'));
+ if (has_service_worker) {
+ `if ('serviceWorker' in navigator) navigator.serviceWorker.register('${req.baseUrl}/service-worker.js')`
+ }
const page = template()
- .replace('%sapper.scripts%', scripts)
+ .replace('%sapper.base%', ``)
+ .replace('%sapper.scripts%', `${scripts}`)
.replace('%sapper.html%', html)
.replace('%sapper.head%', `${head}`)
.replace('%sapper.styles%', (css && css.code ? `` : ''));
@@ -282,7 +301,8 @@ function get_route_handler(chunks: Record, routes: RouteObject[]
const { head, css, html } = rendered;
const page = template()
- .replace('%sapper.scripts%', ``)
+ .replace('%sapper.base%', ``)
+ .replace('%sapper.scripts%', ``)
.replace('%sapper.html%', html)
.replace('%sapper.head%', `${head}`)
.replace('%sapper.styles%', (css && css.code ? `` : ''));
@@ -291,11 +311,9 @@ function get_route_handler(chunks: Record, routes: RouteObject[]
}
return function find_route(req: Req, res: ServerResponse) {
- const url = req.pathname;
-
try {
for (const route of routes) {
- if (!route.error && route.pattern.test(url)) return handle_route(route, req, res);
+ if (!route.error && route.pattern.test(req.path)) return handle_route(route, req, res);
}
handle_error(req, res, 404, 'Not found');
diff --git a/src/runtime/index.ts b/src/runtime/index.ts
index 22cd582..b1cdfe8 100644
--- a/src/runtime/index.ts
+++ b/src/runtime/index.ts
@@ -1,6 +1,8 @@
import { detach, findAnchor, scroll_state, which } from './utils';
import { Component, ComponentConstructor, Params, Query, Route, RouteData, ScrollPosition, Target } from './interfaces';
+const manifest = typeof window !== 'undefined' && window.__SAPPER__;
+
export let component: Component;
let target: Node;
let routes: Route[];
@@ -22,9 +24,12 @@ if ('scrollRestoration' in history) {
function select_route(url: URL): Target {
if (url.origin !== window.location.origin) return null;
+ if (!url.pathname.startsWith(manifest.baseUrl)) return null;
+
+ const pathname = url.pathname.slice(manifest.baseUrl.length);
for (const route of routes) {
- const match = route.pattern.exec(url.pathname);
+ const match = route.pattern.exec(pathname);
if (match) {
if (route.ignore) return null;
@@ -80,8 +85,8 @@ function prepare_route(Component: ComponentConstructor, data: RouteData) {
return { Component, data, redirect, error };
}
- if (!component && window.__SAPPER__ && window.__SAPPER__.preloaded) {
- return { Component, data: Object.assign(data, window.__SAPPER__.preloaded), redirect, error };
+ if (!component && manifest.preloaded) {
+ return { Component, data: Object.assign(data, manifest.preloaded), redirect, error };
}
return Promise.resolve(Component.preload.call({
@@ -203,7 +208,7 @@ let prefetching: {
} = null;
export function prefetch(href: string) {
- const selected = select_route(new URL(href));
+ const selected = select_route(new URL(href, document.baseURI));
if (selected) {
prefetching = {
@@ -257,7 +262,8 @@ export function init(_target: Node, _routes: Route[]) {
}
export function goto(href: string, opts = { replaceState: false }) {
- const target = select_route(new URL(href, window.location.href));
+ const target = select_route(new URL(href, document.baseURI));
+
if (target) {
navigate(target, null);
if (history) history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
diff --git a/src/webpack.ts b/src/webpack.ts
index 55b245c..28fefec 100644
--- a/src/webpack.ts
+++ b/src/webpack.ts
@@ -15,7 +15,7 @@ export default {
path: `${locations.dest()}/client`,
filename: '[hash]/[name].js',
chunkFilename: '[hash]/[name].[id].js',
- publicPath: '/client/'
+ publicPath: `client/`
};
}
},
diff --git a/test/app/app/server.js b/test/app/app/server.js
index 3d9e7db..d122bce 100644
--- a/test/app/app/server.js
+++ b/test/app/app/server.js
@@ -1,6 +1,6 @@
import fs from 'fs';
-import polka from 'polka';
-import compression from 'compression';
+import { resolve } from 'url';
+import express from 'express';
import serve from 'serve-static';
import sapper from '../../../dist/middleware.ts.js';
import { routes } from './manifest/server.js';
@@ -28,58 +28,62 @@ process.on('message', message => {
}
});
-const app = polka();
+const app = express();
-app.use((req, res, next) => {
- if (pending) pending.add(req.url);
-
- const { write, end } = res;
- const chunks = [];
-
- res.write = function(chunk) {
- chunks.push(new Buffer(chunk));
- write.apply(res, arguments);
- };
-
- res.end = function(chunk) {
- if (chunk) chunks.push(new Buffer(chunk));
- end.apply(res, arguments);
-
- if (pending) pending.delete(req.url);
-
- process.send({
- method: req.method,
- url: req.url,
- status: res.statusCode,
- headers: res._headers,
- body: Buffer.concat(chunks).toString()
- });
-
- if (pending && pending.size === 0 && ended) {
- process.send({ type: 'done' });
- }
- };
-
- next();
-});
-
-const { PORT = 3000 } = process.env;
+const { PORT = 3000, BASEPATH = '' } = process.env;
+const base = `http://localhost:${PORT}${BASEPATH}/`;
// this allows us to do e.g. `fetch('/api/blog')` on the server
const fetch = require('node-fetch');
global.fetch = (url, opts) => {
- if (url[0] === '/') url = `http://localhost:${PORT}${url}`;
- return fetch(url, opts);
+ return fetch(resolve(base, url), opts);
};
-app.use(compression({ threshold: 0 }));
+const middlewares = [
+ serve('assets'),
-app.use(serve('assets'));
+ (req, res, next) => {
+ if (!pending) return next();
-app.use(sapper({
- routes
-}));
+ pending.add(req.url);
-app.listen(PORT, () => {
- console.log(`listening on port ${PORT}`);
-});
\ No newline at end of file
+ const { write, end } = res;
+ const chunks = [];
+
+ res.write = function(chunk) {
+ chunks.push(new Buffer(chunk));
+ write.apply(res, arguments);
+ };
+
+ res.end = function(chunk) {
+ if (chunk) chunks.push(new Buffer(chunk));
+ end.apply(res, arguments);
+
+ if (pending) pending.delete(req.url);
+
+ process.send({
+ method: req.method,
+ url: req.url,
+ status: res.statusCode,
+ headers: res._headers,
+ body: Buffer.concat(chunks).toString()
+ });
+
+ if (pending && pending.size === 0 && ended) {
+ process.send({ type: 'done' });
+ }
+ };
+
+ next();
+ },
+
+ sapper({ routes })
+];
+
+if (BASEPATH) {
+ app.use(BASEPATH, ...middlewares);
+} else {
+ app.use(...middlewares);
+}
+
+app.listen(PORT);
\ No newline at end of file
diff --git a/test/app/app/template.html b/test/app/app/template.html
index fc85583..0ea5e17 100644
--- a/test/app/app/template.html
+++ b/test/app/app/template.html
@@ -5,15 +5,11 @@
-
-
-
+ %sapper.base%
-
+
+
+
- {{post.title}}
+ {{post.title}}
{{/each}}
@@ -32,7 +32,7 @@
},
preload({ params, query }) {
- return fetch(`/blog.json`).then(r => r.json()).then(posts => {
+ return fetch(`blog.json`).then(r => r.json()).then(posts => {
return { posts };
});
}
diff --git a/test/app/routes/delete-test.html b/test/app/routes/delete-test.html
index d2df93c..428ab3c 100644
--- a/test/app/routes/delete-test.html
+++ b/test/app/routes/delete-test.html
@@ -4,7 +4,7 @@
export default {
methods: {
del() {
- fetch(`/api/delete/42`, { method: 'DELETE' })
+ fetch(`api/delete/42`, { method: 'DELETE' })
.then(r => r.json())
.then(data => {
window.deleted = data;
diff --git a/test/app/routes/index.html b/test/app/routes/index.html
index bbb024a..10c1f9d 100644
--- a/test/app/routes/index.html
+++ b/test/app/routes/index.html
@@ -6,7 +6,7 @@
Great success!
-
+
HIGH FIVE!
diff --git a/test/app/routes/redirect-from.html b/test/app/routes/redirect-from.html
index ecd5fb9..c787768 100644
--- a/test/app/routes/redirect-from.html
+++ b/test/app/routes/redirect-from.html
@@ -1,7 +1,7 @@
\ No newline at end of file
diff --git a/test/common/test.js b/test/common/test.js
index e40c87b..6f49c79 100644
--- a/test/common/test.js
+++ b/test/common/test.js
@@ -12,6 +12,10 @@ Nightmare.action('page', {
this.evaluate_now(() => document.querySelector('h1').textContent, done);
},
+ html(done) {
+ this.evaluate_now(() => document.documentElement.innerHTML, done);
+ },
+
text(done) {
this.evaluate_now(() => document.body.textContent, done);
}
@@ -35,11 +39,21 @@ describe('sapper', function() {
rimraf.sync('build');
rimraf.sync('.sapper');
- this.timeout(30000);
+ this.timeout(process.env.CI ? 30000 : 5000);
// TODO reinstate dev tests
- // run('development');
- run('production');
+ // run({
+ // mode: 'development'
+ // });
+
+ run({
+ mode: 'production'
+ });
+
+ run({
+ mode: 'production',
+ basepath: '/custom-basepath'
+ });
describe('export', () => {
before(() => {
@@ -111,16 +125,29 @@ describe('sapper', function() {
});
});
-function run(env) {
- describe(`env=${env}`, function () {
+function run({ mode, basepath = '' }) {
+ describe(`mode=${mode}`, function () {
let proc;
- let nightmare;
let capture;
let base;
+ const nightmare = new Nightmare();
+
+ nightmare.on('console', (type, ...args) => {
+ console[type](...args);
+ });
+
+ nightmare.on('page', (type, ...args) => {
+ if (type === 'error') {
+ console.error(args[1]);
+ } else {
+ console.warn(type, args);
+ }
+ });
+
before(() => {
- const promise = env === 'production'
+ const promise = mode === 'production'
? exec(`node ${cli} build`).then(() => ports.find(3000))
: ports.find(3000).then(port => {
exec(`node ${cli} dev`);
@@ -129,13 +156,15 @@ function run(env) {
return promise.then(port => {
base = `http://localhost:${port}`;
+ if (basepath) base += basepath;
- const dir = env === 'production' ? 'build' : '.sapper';
+ const dir = mode === 'production' ? 'build' : '.sapper';
proc = require('child_process').fork(`${dir}/server.js`, {
cwd: process.cwd(),
env: {
- NODE_ENV: env,
+ NODE_ENV: mode,
+ BASEPATH: basepath,
SAPPER_DEST: dir,
PORT: port
}
@@ -183,30 +212,13 @@ function run(env) {
after(() => {
// give a chance to clean up
- return new Promise(fulfil => {
- proc.on('exit', fulfil);
- proc.kill();
- });
- });
-
- beforeEach(() => {
- nightmare = new Nightmare();
-
- nightmare.on('console', (type, ...args) => {
- console[type](...args);
- });
-
- nightmare.on('page', (type, ...args) => {
- if (type === 'error') {
- console.error(args[1]);
- } else {
- console.warn(type, args);
- }
- });
- });
-
- afterEach(() => {
- return nightmare.end();
+ return Promise.all([
+ nightmare.end(),
+ new Promise(fulfil => {
+ proc.on('exit', fulfil);
+ proc.kill();
+ })
+ ]);
});
describe('basic functionality', () => {
@@ -235,16 +247,16 @@ function run(env) {
});
it('navigates to a new page without reloading', () => {
- return capture(() => nightmare.goto(base).init().prefetchRoutes())
+ return nightmare.goto(base).init().prefetchRoutes()
.then(() => {
- return capture(() => nightmare.click('a[href="/about"]'));
+ return capture(() => nightmare.click('a[href="about"]'));
})
.then(requests => {
assert.deepEqual(requests.map(r => r.url), []);
return nightmare.path();
})
.then(path => {
- assert.equal(path, '/about');
+ assert.equal(path, `${basepath}/about`);
return nightmare.title();
})
.then(title => {
@@ -257,7 +269,7 @@ function run(env) {
.goto(`${base}/about`)
.init()
.click('.goto')
- .wait(() => window.location.pathname === '/blog/what-is-sapper')
+ .wait(url => window.location.pathname === url, `${basepath}/blog/what-is-sapper`)
.wait(100)
.title()
.then(title => {
@@ -266,9 +278,7 @@ function run(env) {
});
it('prefetches programmatically', () => {
- return nightmare
- .goto(`${base}/about`)
- .init()
+ return capture(() => nightmare.goto(`${base}/about`).init())
.then(() => {
return capture(() => {
return nightmare
@@ -277,7 +287,7 @@ function run(env) {
});
})
.then(requests => {
- assert.ok(!!requests.find(r => r.url === '/blog/why-the-name.json'));
+ assert.ok(!!requests.find(r => r.url === `/blog/why-the-name.json`));
});
});
@@ -298,21 +308,21 @@ function run(env) {
.then(() => {
return capture(() => {
return nightmare
- .mouseover('[href="/blog/what-is-sapper"]')
+ .mouseover('[href="blog/what-is-sapper"]')
.wait(200);
});
})
.then(mouseover_requests => {
- assert.ok(mouseover_requests.findIndex(r => r.url === '/blog/what-is-sapper.json') !== -1);
+ assert.ok(mouseover_requests.findIndex(r => r.url === `/blog/what-is-sapper.json`) !== -1);
return capture(() => {
return nightmare
- .click('[href="/blog/what-is-sapper"]')
+ .click('[href="blog/what-is-sapper"]')
.wait(200);
});
})
.then(click_requests => {
- assert.ok(click_requests.findIndex(r => r.url === '/blog/what-is-sapper.json') === -1);
+ assert.ok(click_requests.findIndex(r => r.url === `/blog/what-is-sapper.json`) === -1);
});
});
@@ -320,13 +330,13 @@ function run(env) {
return nightmare
.goto(base)
.init()
- .click('a[href="/slow-preload"]')
+ .click('a[href="slow-preload"]')
.wait(100)
- .click('a[href="/about"]')
+ .click('a[href="about"]')
.wait(100)
.then(() => nightmare.path())
.then(path => {
- assert.equal(path, '/about');
+ assert.equal(path, `${basepath}/about`);
return nightmare.title();
})
.then(title => {
@@ -335,7 +345,7 @@ function run(env) {
})
.then(() => nightmare.path())
.then(path => {
- assert.equal(path, '/about');
+ assert.equal(path, `${basepath}/about`);
return nightmare.title();
})
.then(title => {
@@ -348,7 +358,7 @@ function run(env) {
.goto(`${base}/show-url`)
.init()
.evaluate(() => document.querySelector('p').innerHTML)
- .end().then(html => {
+ .then(html => {
assert.equal(html, `URL is /show-url`);
});
});
@@ -384,7 +394,7 @@ function run(env) {
return nightmare.goto(`${base}/redirect-from`)
.path()
.then(path => {
- assert.equal(path, '/redirect-to');
+ assert.equal(path, `${basepath}/redirect-to`);
})
.then(() => nightmare.page.title())
.then(title => {
@@ -394,12 +404,12 @@ function run(env) {
it('redirects in client', () => {
return nightmare.goto(base)
- .wait('[href="/redirect-from"]')
- .click('[href="/redirect-from"]')
+ .wait('[href="redirect-from"]')
+ .click('[href="redirect-from"]')
.wait(200)
.path()
.then(path => {
- assert.equal(path, '/redirect-to');
+ assert.equal(path, `${basepath}/redirect-to`);
})
.then(() => nightmare.page.title())
.then(title => {
@@ -411,7 +421,7 @@ function run(env) {
return nightmare.goto(`${base}/blog/nope`)
.path()
.then(path => {
- assert.equal(path, '/blog/nope');
+ assert.equal(path, `${basepath}/blog/nope`);
})
.then(() => nightmare.page.title())
.then(title => {
@@ -422,11 +432,11 @@ function run(env) {
it('handles 4xx error in client', () => {
return nightmare.goto(base)
.init()
- .click('[href="/blog/nope"]')
+ .click('[href="blog/nope"]')
.wait(200)
.path()
.then(path => {
- assert.equal(path, '/blog/nope');
+ assert.equal(path, `${basepath}/blog/nope`);
})
.then(() => nightmare.page.title())
.then(title => {
@@ -438,7 +448,7 @@ function run(env) {
return nightmare.goto(`${base}/blog/throw-an-error`)
.path()
.then(path => {
- assert.equal(path, '/blog/throw-an-error');
+ assert.equal(path, `${basepath}/blog/throw-an-error`);
})
.then(() => nightmare.page.title())
.then(title => {
@@ -449,11 +459,11 @@ function run(env) {
it('handles non-4xx error in client', () => {
return nightmare.goto(base)
.init()
- .click('[href="/blog/throw-an-error"]')
+ .click('[href="blog/throw-an-error"]')
.wait(200)
.path()
.then(path => {
- assert.equal(path, '/blog/throw-an-error');
+ assert.equal(path, `${basepath}/blog/throw-an-error`);
})
.then(() => nightmare.page.title())
.then(title => {
@@ -464,7 +474,7 @@ function run(env) {
it('does not attempt client-side navigation to server routes', () => {
return nightmare.goto(`${base}/blog/how-is-sapper-different-from-next`)
.init()
- .click(`[href="/blog/how-is-sapper-different-from-next.json"]`)
+ .click(`[href="blog/how-is-sapper-different-from-next.json"]`)
.wait(200)
.page.text()
.then(text => {
@@ -515,7 +525,7 @@ function run(env) {
describe('headers', () => {
it('sets Content-Type and Link...preload headers', () => {
- return capture(() => nightmare.goto(base).end()).then(requests => {
+ return capture(() => nightmare.goto(base)).then(requests => {
const { headers } = requests[0];
assert.equal(
@@ -523,8 +533,16 @@ function run(env) {
'text/html'
);
+ const str = ['main', '_\\.\\d+']
+ .map(file => {
+ return `<${basepath}/client/[^/]+/${file}\\.js>;rel="preload";as="script"`;
+ })
+ .join(', ');
+
+ const regex = new RegExp(str);
+
assert.ok(
- /<\/client\/[^/]+\/main\.js>;rel="preload";as="script", <\/client\/[^/]+\/_\.\d+\.js>;rel="preload";as="script"/.test(headers['link']),
+ regex.test(headers['link']),
headers['link']
);
});
@@ -535,7 +553,7 @@ function run(env) {
function exec(cmd) {
return new Promise((fulfil, reject) => {
- const parts = cmd.split(' ');
+ const parts = cmd.trim().split(' ');
const proc = require('child_process').spawn(parts.shift(), parts);
proc.stdout.on('data', data => {