From b94481b716109be09805be6ad56e6285b1982586 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 17 Mar 2018 11:55:02 -0400 Subject: [PATCH] =?UTF-8?q?Support=20being=20mounted=20on=20a=20path=20?= =?UTF-8?q?=E2=80=94=20fixes=20#180?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 7 +- package.json | 1 + runtime.js | 271 --------------------------- src/cli.ts | 5 +- src/cli/build.ts | 2 +- src/cli/dev.ts | 2 +- src/cli/export.ts | 55 +++--- src/middleware.ts | 48 +++-- src/runtime/index.ts | 16 +- src/webpack.ts | 2 +- test/app/app/server.js | 90 ++++----- test/app/app/template.html | 12 +- test/app/routes/_components/Nav.html | 14 +- test/app/routes/about.html | 4 +- test/app/routes/blog/[slug].html | 2 +- test/app/routes/blog/_posts.js | 6 +- test/app/routes/blog/index.html | 4 +- test/app/routes/delete-test.html | 2 +- test/app/routes/index.html | 2 +- test/app/routes/redirect-from.html | 2 +- test/common/test.js | 146 ++++++++------- 21 files changed, 236 insertions(+), 457 deletions(-) delete mode 100644 runtime.js diff --git a/.gitignore b/.gitignore index 57aee3fb3..8ac550455 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 1550578ee..37a0b75a8 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 a3db0606c..000000000 --- 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 4be699c98..8913b9f5e 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 027cbace9..4bafa49f9 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 9ca6f63f7..8b358e31d 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 38e466fd1..95a155d5a 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 (range >= 4) { + console.log(`${clorox.red(`> Received ${r.status} response when fetching ${url.pathname}`)}`); + return; + } - if (seen.has(url.pathname)) return; - seen.add(url.pathname); + if (range === 2) { + if (r.headers.get('Content-Type') === 'text/html') { + const body = await r.text(); + const $ = cheerio.load(body); + const urls: URL[] = []; - 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[] = []; + const base = new URL($('base').attr('href') || '/', url.href); - $('a[href]').each((i: number, $a) => { - hrefs.push($a.attribs.href); - }); + $('a[href]').each((i: number, $a) => { + const url = new URL($a.attribs.href, base.href); - return hrefs.reduce((promise, href) => { - return promise.then(() => handle(new URL(href, url.href))); - }, Promise.resolve()); - }); + 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 bc23a3584..cdfde823e 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 22cd582df..b1cdfe8db 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 55b245c38..28fefec45 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 3d9e7db50..d122bce09 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 { PORT = 3000, BASEPATH = '' } = process.env; +const base = `http://localhost:${PORT}${BASEPATH}/`; - const { write, end } = res; - const chunks = []; +// this allows us to do e.g. `fetch('/api/blog')` on the server +const fetch = require('node-fetch'); +global.fetch = (url, opts) => { + return fetch(resolve(base, url), opts); +}; - res.write = function(chunk) { - chunks.push(new Buffer(chunk)); - write.apply(res, arguments); - }; +const middlewares = [ + serve('assets'), - res.end = function(chunk) { - if (chunk) chunks.push(new Buffer(chunk)); - end.apply(res, arguments); + (req, res, next) => { + if (!pending) return next(); - if (pending) pending.delete(req.url); + pending.add(req.url); - process.send({ - method: req.method, - url: req.url, - status: res.statusCode, - headers: res._headers, - body: Buffer.concat(chunks).toString() - }); + const { write, end } = res; + const chunks = []; - if (pending && pending.size === 0 && ended) { - process.send({ type: 'done' }); - } - }; + res.write = function(chunk) { + chunks.push(new Buffer(chunk)); + write.apply(res, arguments); + }; - next(); -}); + res.end = function(chunk) { + if (chunk) chunks.push(new Buffer(chunk)); + end.apply(res, arguments); -const { PORT = 3000 } = process.env; + if (pending) pending.delete(req.url); -// 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); -}; + 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' }); + } + }; -app.use(compression({ threshold: 0 })); + next(); + }, -app.use(serve('assets')); + sapper({ routes }) +]; -app.use(sapper({ - routes -})); +if (BASEPATH) { + app.use(BASEPATH, ...middlewares); +} else { + app.use(...middlewares); +} -app.listen(PORT, () => { - console.log(`listening on port ${PORT}`); -}); \ No newline at end of file +app.listen(PORT); \ No newline at end of file diff --git a/test/app/app/template.html b/test/app/app/template.html index fc8558380..0ea5e179e 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 @@

    Recent posts

    }, 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 d2df93c6a..428ab3c96 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 bbb024a43..10c1f9d39 100644 --- a/test/app/routes/index.html +++ b/test/app/routes/index.html @@ -6,7 +6,7 @@

    Great success!

    - borat + borat
    HIGH FIVE!
    diff --git a/test/app/routes/redirect-from.html b/test/app/routes/redirect-from.html index ecd5fb983..c787768c4 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 e40c87b73..6f49c7997 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 => {