Merge branch 'master' into site

This commit is contained in:
Richard Harris
2019-05-01 09:01:39 -04:00
43 changed files with 278 additions and 104 deletions

View File

@@ -1,4 +1,5 @@
--require source-map-support/register --require source-map-support/register
--require sucrase/register --require sucrase/register
--recursive --recursive
test/unit/*/test.ts
test/apps/*/test.ts test/apps/*/test.ts

5
package-lock.json generated
View File

@@ -5530,9 +5530,8 @@
} }
}, },
"yootils": { "yootils": {
"version": "0.0.14", "version": "0.0.15",
"resolved": "https://registry.npmjs.org/yootils/-/yootils-0.0.14.tgz", "resolved": "github:bwbroersma/yootils#77a0949b90387af0bff8081cf596a752a1a3e08e",
"integrity": "sha512-yWoA/a/4aVUp5nqfqdjbTdyXcR8d0OAbRQ8Ktu3ZsfQnArwLpS81oqZl3adIszX3p8NEhT0aNHARPsaTwBH/Qw==",
"dev": true "dev": true
} }
} }

View File

@@ -58,7 +58,7 @@
"svelte-loader": "^2.13.3", "svelte-loader": "^2.13.3",
"webpack": "^4.29.0", "webpack": "^4.29.0",
"webpack-format-messages": "^2.0.5", "webpack-format-messages": "^2.0.5",
"yootils": "0.0.14" "yootils": "0.0.15"
}, },
"peerDependencies": { "peerDependencies": {
"svelte": "^3.0.0" "svelte": "^3.0.0"

View File

@@ -1,12 +0,0 @@
<script>
import { setContext } from 'svelte';
import { CONTEXT_KEY } from './shared';
export let Root;
export let props;
export let session;
setContext(CONTEXT_KEY, session);
</script>
<Root {...props}/>

View File

@@ -1,10 +1,5 @@
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
export const stores = {
preloading: writable(false),
page: writable(null)
};
export const CONTEXT_KEY = {}; export const CONTEXT_KEY = {};
export const preload = () => ({}); export const preload = () => ({});

View File

@@ -1,7 +1,6 @@
import { writable } from 'svelte/store.mjs'; import { writable } from 'svelte/store.mjs';
import App from '@sapper/internal/App.svelte'; import App from '@sapper/internal/App.svelte';
import { stores } from '@sapper/internal/shared'; import { root_preload, ErrorComponent, ignore, components, routes } from '@sapper/internal/manifest-client';
import { Root, root_preload, ErrorComponent, ignore, components, routes } from '@sapper/internal/manifest-client';
import { import {
Target, Target,
ScrollPosition, ScrollPosition,
@@ -24,12 +23,16 @@ let current_token: {};
let root_preloaded: Promise<any>; let root_preloaded: Promise<any>;
let current_branch = []; let current_branch = [];
const session = writable(initial_data && initial_data.session); const stores = {
page: writable({}),
preloading: writable(null),
session: writable(initial_data && initial_data.session)
};
let $session; let $session;
let session_dirty: boolean; let session_dirty: boolean;
session.subscribe(async value => { stores.session.subscribe(async value => {
$session = value; $session = value;
if (!ready) return; if (!ready) return;
@@ -85,8 +88,7 @@ export function extract_query(search: string) {
const query = Object.create(null); const query = Object.create(null);
if (search.length > 0) { if (search.length > 0) {
search.slice(1).split('&').forEach(searchParam => { search.slice(1).split('&').forEach(searchParam => {
let [, key, value] = /([^=]*)(?:=(.*))?/.exec(decodeURIComponent(searchParam)); let [, key, value = ''] = /([^=]*)(?:=(.*))?/.exec(decodeURIComponent(searchParam.replace(/\+/g, ' ')));
value = (value || '').replace(/\+/g, ' ');
if (typeof query[key] === 'string') query[key] = [<string>query[key]]; if (typeof query[key] === 'string') query[key] = [<string>query[key]];
if (typeof query[key] === 'object') (query[key] as string[]).push(value); if (typeof query[key] === 'object') (query[key] as string[]).push(value);
else query[key] = value; else query[key] = value;
@@ -217,7 +219,11 @@ async function render(redirect: Redirect, branch: any[], props: any, page: Page)
if (root_component) { if (root_component) {
root_component.$set(props); root_component.$set(props);
} else { } else {
props.session = session; props.stores = {
page: { subscribe: stores.page.subscribe },
preloading: { subscribe: stores.preloading.subscribe },
session: stores.session
};
props.level0 = { props.level0 = {
props: await root_preloaded props: await root_preloaded
}; };

View File

@@ -1,10 +1,7 @@
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import { CONTEXT_KEY, stores } from '@sapper/internal/shared'; import { CONTEXT_KEY } from '@sapper/internal/shared';
export const preloading = { subscribe: stores.preloading.subscribe }; export const stores = () => getContext(CONTEXT_KEY);
export const page = { subscribe: stores.page.subscribe };
export const getSession = () => getContext(CONTEXT_KEY);
export { default as start } from './start/index'; export { default as start } from './start/index';
export { default as goto } from './goto/index'; export { default as goto } from './goto/index';

View File

@@ -177,7 +177,7 @@ export function get_page_handler(
try { try {
if (redirect) { if (redirect) {
const location = URL.resolve(req.baseUrl || '/', redirect.location); const location = URL.resolve((req.baseUrl || '') + '/', redirect.location);
res.statusCode = redirect.statusCode; res.statusCode = redirect.statusCode;
res.setHeader('Location', location); res.setHeader('Location', location);
@@ -204,10 +204,22 @@ export function get_page_handler(
}); });
const props = { const props = {
stores: {
page: {
subscribe: writable({
path: req.path,
query: req.query,
params
}).subscribe
},
preloading: {
subscribe: writable(null).subscribe
},
session: writable(session)
},
segments: layout_segments, segments: layout_segments,
status: error ? status : 200, status: error ? status : 200,
error: error ? error instanceof Error ? error : { message: error } : null, error: error ? error instanceof Error ? error : { message: error } : null,
session: writable(session),
level0: { level0: {
props: preloaded[0] props: preloaded[0]
}, },
@@ -231,12 +243,6 @@ export function get_page_handler(
} }
} }
stores.page.set({
path: req.path,
query: req.query,
params: params
});
const { html, head, css } = App.render(props); const { html, head, css } = App.render(props);
const serialized = { const serialized = {

View File

@@ -19,6 +19,7 @@ type Opts = {
static?: string, static?: string,
basepath?: string, basepath?: string,
timeout?: number | false, timeout?: number | false,
concurrent?: number,
oninfo?: ({ message }: { message: string }) => void; oninfo?: ({ message }: { message: string }) => void;
onfile?: ({ file, size, status }: { file: string, size: number, status: number }) => void; onfile?: ({ file, size, status }: { file: string, size: number, status: number }) => void;
}; };
@@ -44,6 +45,7 @@ async function _export({
export_dir = '__sapper__/export', export_dir = '__sapper__/export',
basepath = '', basepath = '',
timeout = 5000, timeout = 5000,
concurrent = 8,
oninfo = noop, oninfo = noop,
onfile = noop onfile = noop
}: Opts = {}) { }: Opts = {}) {
@@ -87,6 +89,7 @@ async function _export({
const seen = new Set(); const seen = new Set();
const saved = new Set(); const saved = new Set();
const q = yootils.queue(concurrent);
function save(url: string, status: number, type: string, body: string) { function save(url: string, status: number, type: string, body: string) {
const { pathname } = resolve(origin, url); const { pathname } = resolve(origin, url);
@@ -123,7 +126,7 @@ async function _export({
async function handle(url: URL) { async function handle(url: URL) {
let pathname = url.pathname; let pathname = url.pathname;
if (pathname !== '/service-worker-index.html') { if (pathname !== '/service-worker-index.html') {
pathname = pathname.replace(root.pathname, '') || '/' pathname = pathname.replace(root.pathname, '') || '/'
} }
if (seen.has(pathname)) return; if (seen.has(pathname)) return;
@@ -135,9 +138,9 @@ async function _export({
}, timeout); }, timeout);
const r = await Promise.race([ const r = await Promise.race([
fetch(url.href, { q.add(() => fetch(url.href, {
redirect: 'manual' redirect: 'manual'
}), })),
timeout_deferred.promise timeout_deferred.promise
]); ]);
@@ -159,11 +162,10 @@ async function _export({
`<link rel="preload" as=${JSON.stringify(ref.as)} href=${JSON.stringify(ref.uri)}></head>`) `<link rel="preload" as=${JSON.stringify(ref.as)} href=${JSON.stringify(ref.uri)}></head>`)
} }
}); });
if (pathname !== '/service-worker-index.html') { if (pathname !== '/service-worker-index.html') {
const cleaned = clean_html(body); const cleaned = clean_html(body);
const q = yootils.queue(8);
const base_match = /<base ([\s\S]+?)>/m.exec(cleaned); const base_match = /<base ([\s\S]+?)>/m.exec(cleaned);
const base_href = base_match && get_href(base_match[1]); const base_href = base_match && get_href(base_match[1]);
const base = resolve(url.href, base_href); const base = resolve(url.href, base_href);
@@ -171,6 +173,8 @@ async function _export({
let match; let match;
let pattern = /<a ([\s\S]+?)>/gm; let pattern = /<a ([\s\S]+?)>/gm;
let promise;
while (match = pattern.exec(cleaned)) { while (match = pattern.exec(cleaned)) {
const attrs = match[1]; const attrs = match[1];
const href = get_href(attrs); const href = get_href(attrs);
@@ -179,12 +183,12 @@ async function _export({
const url = resolve(base.href, href); const url = resolve(base.href, href);
if (url.protocol === protocol && url.host === host) { if (url.protocol === protocol && url.host === host) {
q.add(() => handle(url)); promise = handle(url);
} }
} }
} }
await q.close(); await promise;
} }
} }
} }
@@ -201,14 +205,17 @@ async function _export({
save(pathname, r.status, type, body); save(pathname, r.status, type, body);
} }
return ports.wait(port) try {
.then(() => handle(root)) await ports.wait(port);
.then(() => handle(resolve(root.href, 'service-worker-index.html'))) await handle(root);
.then(() => proc.kill()) await handle(resolve(root.href, 'service-worker-index.html'));
.catch(err => { await q.close();
proc.kill();
throw err; proc.kill()
}); } catch (err) {
proc.kill();
throw err;
}
} }
function get_href(attrs: string) { function get_href(attrs: string) {

View File

@@ -189,6 +189,7 @@ prog.command('export [dest]')
.describe('Export your app as static files (if possible)') .describe('Export your app as static files (if possible)')
.option('--build', '(Re)build app before exporting', true) .option('--build', '(Re)build app before exporting', true)
.option('--basepath', 'Specify a base path') .option('--basepath', 'Specify a base path')
.option('--concurrent', 'Concurrent requests', 8)
.option('--timeout', 'Milliseconds to wait for a page (--no-timeout to disable)', 5000) .option('--timeout', 'Milliseconds to wait for a page (--no-timeout to disable)', 5000)
.option('--legacy', 'Create separate legacy build') .option('--legacy', 'Create separate legacy build')
.option('--bundler', 'Specify a bundler (rollup or webpack, blank for auto)') .option('--bundler', 'Specify a bundler (rollup or webpack, blank for auto)')
@@ -203,6 +204,7 @@ prog.command('export [dest]')
legacy: boolean, legacy: boolean,
bundler?: 'rollup' | 'webpack', bundler?: 'rollup' | 'webpack',
basepath?: string, basepath?: string,
concurrent: number,
timeout: number | false, timeout: number | false,
cwd: string, cwd: string,
src: string, src: string,
@@ -228,6 +230,7 @@ prog.command('export [dest]')
export_dir: dest, export_dir: dest,
basepath: opts.basepath, basepath: opts.basepath,
timeout: opts.timeout, timeout: opts.timeout,
concurrent: opts.concurrent,
oninfo: event => { oninfo: event => {
console.log(colors.bold().cyan(`> ${event.message}`)); console.log(colors.bold().cyan(`> ${event.message}`));

View File

@@ -70,6 +70,12 @@ export function create_serviceworker_manifest({ manifest_data, output, client_fi
write_if_changed(`${output}/service-worker.js`, code); write_if_changed(`${output}/service-worker.js`, code);
} }
function create_param_match(param: string, i: number) {
return /^\.{3}.+$/.test(param)
? `${param.replace(/.{3}/, '')}: d(match[${i + 1}]).split('/')`
: `${param}: d(match[${i + 1}])`
}
function generate_client_manifest( function generate_client_manifest(
manifest_data: ManifestData, manifest_data: ManifestData,
path_to_routes: string, path_to_routes: string,
@@ -114,7 +120,7 @@ function generate_client_manifest(
if (part.params.length > 0) { if (part.params.length > 0) {
needs_decode = true; needs_decode = true;
const props = part.params.map((param, i) => `${param}: d(match[${i + 1}])`); const props = part.params.map(create_param_match);
return `{ i: ${component_indexes[part.component.name]}, params: match => ({ ${props.join(', ')} }) }`; return `{ i: ${component_indexes[part.component.name]}, params: match => ({ ${props.join(', ')} }) }`;
} }
@@ -189,7 +195,7 @@ function generate_server_manifest(
pattern: ${route.pattern}, pattern: ${route.pattern},
handlers: route_${i}, handlers: route_${i},
params: ${route.params.length > 0 params: ${route.params.length > 0
? `match => ({ ${route.params.map((param, i) => `${param}: d(match[${i + 1}])`).join(', ')} })` ? `match => ({ ${route.params.map(create_param_match).join(', ')} })`
: `() => ({})`} : `() => ({})`}
}`).join(',\n\n\t\t\t\t')} }`).join(',\n\n\t\t\t\t')}
], ],
@@ -210,7 +216,7 @@ function generate_server_manifest(
].filter(Boolean); ].filter(Boolean);
if (part.params.length > 0) { if (part.params.length > 0) {
const params = part.params.map((param, i) => `${param}: d(match[${i + 1}])`); const params = part.params.map(create_param_match);
props.push(`params: match => ({ ${params.join(', ')} })`); props.push(`params: match => ({ ${params.join(', ')} })`);
} }
@@ -265,14 +271,14 @@ function generate_app(manifest_data: ManifestData, path_to_routes: string) {
import Layout from '${get_file(path_to_routes, manifest_data.root)}'; import Layout from '${get_file(path_to_routes, manifest_data.root)}';
import Error from '${get_file(path_to_routes, manifest_data.error)}'; import Error from '${get_file(path_to_routes, manifest_data.error)}';
export let session; export let stores;
export let error; export let error;
export let status; export let status;
export let segments; export let segments;
export let level0; export let level0;
${levels.map(l => `export let level${l} = null;`).join('\n\t\t\t')} ${levels.map(l => `export let level${l} = null;`).join('\n\t\t\t')}
setContext(CONTEXT_KEY, session); setContext(CONTEXT_KEY, stores);
</script> </script>
<Layout segment={segments[0]} {...level0.props}> <Layout segment={segments[0]} {...level0.props}>

View File

@@ -246,13 +246,23 @@ type Part = {
content: string; content: string;
dynamic: boolean; dynamic: boolean;
qualifier?: string; qualifier?: string;
spread?: boolean;
}; };
function is_spead(path: string) {
const spread_pattern = /\[\.{3}/g;
return spread_pattern.test(path)
}
function comparator( function comparator(
a: { basename: string, parts: Part[], file: string, is_index: boolean }, a: { basename: string, parts: Part[], file: string, is_index: boolean },
b: { basename: string, parts: Part[], file: string, is_index: boolean } b: { basename: string, parts: Part[], file: string, is_index: boolean }
) { ) {
if (a.is_index !== b.is_index) return a.is_index ? -1 : 1; if (a.is_index !== b.is_index) {
if (a.is_index) return is_spead(a.file) ? 1 : -1;
return is_spead(b.file) ? -1 : 1;
}
const max = Math.max(a.parts.length, b.parts.length); const max = Math.max(a.parts.length, b.parts.length);
@@ -263,6 +273,14 @@ function comparator(
if (!a_sub_part) return 1; // b is more specific, so goes first if (!a_sub_part) return 1; // b is more specific, so goes first
if (!b_sub_part) return -1; if (!b_sub_part) return -1;
// if spread && index, order later
if (a_sub_part.spread && b_sub_part.spread) {
return a.is_index ? 1 : -1
}
// If one is ...spread order it later
if (a_sub_part.spread !== b_sub_part.spread) return a_sub_part.spread ? 1 : -1;
if (a_sub_part.dynamic !== b_sub_part.dynamic) { if (a_sub_part.dynamic !== b_sub_part.dynamic) {
return a_sub_part.dynamic ? 1 : -1; return a_sub_part.dynamic ? 1 : -1;
} }
@@ -306,6 +324,7 @@ function get_parts(part: string): Part[] {
return { return {
content, content,
dynamic, dynamic,
spread: /^\.{3}.+$/.test(content),
qualifier qualifier
}; };
}) })
@@ -333,7 +352,7 @@ function get_pattern(segments: Part[][], add_trailing_slash: boolean) {
segments.map(segment => { segments.map(segment => {
return '\\/' + segment.map(part => { return '\\/' + segment.map(part => {
return part.dynamic return part.dynamic
? part.qualifier || '([^\\/]+?)' ? part.qualifier || part.spread ? '(.+)' : '([^\\/]+?)'
: encodeURI(part.content.normalize()) : encodeURI(part.content.normalize())
.replace(/\?/g, '%3F') .replace(/\?/g, '%3F')
.replace(/#/g, '%23') .replace(/#/g, '%23')

View File

@@ -0,0 +1,3 @@
export function get(req, res) {
res.end(req.params.rest.join(','));
}

View File

@@ -0,0 +1,8 @@
<script>
import { stores } from '@sapper/app';
const { page } = stores();
</script>
<h1>{$page.params.rest.join(',')}</h1>
<a href="xyz/abc/qwe/deep.json">deep</a>

View File

@@ -0,0 +1,8 @@
<script>
import { stores } from '@sapper/app';
const { page } = stores();
</script>
<h1>{$page.params.rest.join(',')}</h1>
<a href="xyz/abc/deep">deep</a>

View File

@@ -1,5 +1,6 @@
<script> <script>
import { page } from '@sapper/app'; import { stores } from '@sapper/app';
const { page } = stores();
</script> </script>
<h1>{$page.params.slug.toUpperCase()}</h1> <h1>{$page.params.slug.toUpperCase()}</h1>

View File

@@ -1,5 +1,6 @@
<script> <script>
import { page } from '@sapper/app'; import { stores } from '@sapper/app';
const { page } = stores();
</script> </script>
<h1>{JSON.stringify($page.query)}</h1> <h1>{JSON.stringify($page.query)}</h1>

View File

@@ -282,6 +282,24 @@ describe('basics', function() {
assert.equal(await title(), 'bar'); assert.equal(await title(), 'bar');
}); });
it('navigates to ...rest', async () => {
await page.goto(`${base}/abc/xyz`);
await start();
assert.equal(await title(), 'abc,xyz');
await page.click('[href="xyz/abc/deep"]');
await wait(50);
assert.equal(await title(), 'xyz,abc');
await page.click(`[href="xyz/abc/qwe/deep.json"]`);
await wait(50);
assert.equal(
await page.evaluate(() => document.body.textContent),
'xyz,abc,qwe'
);
});
it('navigates between dynamic routes with same segments', async () => { it('navigates between dynamic routes with same segments', async () => {
await page.goto(`${base}/dirs/bar/xyz`); await page.goto(`${base}/dirs/bar/xyz`);
await start(); await start();

View File

@@ -7,7 +7,10 @@
</script> </script>
<script> <script>
import { page } from '@sapper/app'; import { stores } from '@sapper/app';
const { page } = stores();
export let slug; export let slug;
</script> </script>

View File

@@ -1,3 +1,3 @@
<h1>Great success!</h1> <h1>Great success!</h1>
<a href="echo/page/encöded?message=hëllö+wörld&föo=bar&=baz">link</a> <a href="echo/page/encöded?message=hëllö+wörld&föo=bar&=baz&tel=%2B123456789">link</a>

View File

@@ -35,11 +35,11 @@ describe('encoding', function() {
}); });
it('encodes req.params and req.query for server-rendered pages', async () => { it('encodes req.params and req.query for server-rendered pages', async () => {
await page.goto(`${base}/echo/page/encöded?message=hëllö+wörld&föo=bar&=baz`); await page.goto(`${base}/echo/page/encöded?message=hëllö+wörld&föo=bar&=baz&tel=%2B123456789`);
assert.equal( assert.equal(
await page.$eval('h1', node => node.textContent), await page.$eval('h1', node => node.textContent),
'encöded {"message":"hëllö wörld","föo":"bar","":"baz"}' 'encöded {"message":"hëllö wörld","föo":"bar","":"baz","tel":"+123456789"}'
); );
}); });
@@ -48,12 +48,12 @@ describe('encoding', function() {
await start(); await start();
await prefetchRoutes(); await prefetchRoutes();
await page.click('a[href="echo/page/encöded?message=hëllö+wörld&föo=bar&=baz"]'); await page.click('a');
await wait(50); await wait(50);
assert.equal( assert.equal(
await page.$eval('h1', node => node.textContent), await page.$eval('h1', node => node.textContent),
'encöded {"message":"hëllö wörld","föo":"bar","":"baz"}' 'encöded {"message":"hëllö wörld","föo":"bar","":"baz","tel":"+123456789"}'
); );
}); });

View File

@@ -0,0 +1,12 @@
<script context="module">
export function preload({ params }) {
return params;
}
</script>
<script>
export let a;
export let b;
</script>
<p>{a}/{b}</p>

View File

@@ -0,0 +1,15 @@
<script context="module">
export function preload({ params }) {
return params;
}
</script>
<script>
export let a;
const list = Array(20).fill().map((_, i) => i + 1);
</script>
{#each list as b}
<a href="boom/{a}/{b}">{a}/{b}</a>
{/each}

View File

@@ -0,0 +1,7 @@
<script>
const list = Array(20).fill().map((_, i) => i + 1);
</script>
{#each list as a}
<a href="boom/{a}">{a}</a>
{/each}

View File

@@ -7,3 +7,4 @@
<a href= >empty anchor #4</a> <a href= >empty anchor #4</a>
<a href>empty anchor #5</a> <a href>empty anchor #5</a>
<a>empty anchor #6</a> <a>empty anchor #6</a>
<a href="boom">boom</a>

View File

@@ -19,7 +19,15 @@ describe('export', function() {
assert.ok(client_assets.length > 0); assert.ok(client_assets.length > 0);
assert.deepEqual(non_client_assets, [ const boom = ['boom/index.html'];
for (let a = 1; a <= 20; a += 1) {
boom.push(`boom/${a}/index.html`);
for (let b = 1; b <= 20; b += 1) {
boom.push(`boom/${a}/${b}/index.html`);
}
}
assert.deepEqual(non_client_assets.sort(), [
'blog.json', 'blog.json',
'blog/bar.json', 'blog/bar.json',
'blog/bar/index.html', 'blog/bar/index.html',
@@ -31,8 +39,9 @@ describe('export', function() {
'global.css', 'global.css',
'index.html', 'index.html',
'service-worker-index.html', 'service-worker-index.html',
'service-worker.js' 'service-worker.js',
]); ...boom
].sort());
}); });
// TODO test timeout, basepath // TODO test timeout, basepath

View File

@@ -9,7 +9,8 @@
</script> </script>
<script> <script>
import { page } from '@sapper/app'; import { stores } from '@sapper/app';
const { page } = stores();
export let count; export let count;
</script> </script>

View File

@@ -9,7 +9,8 @@
</script> </script>
<script> <script>
import { page } from '@sapper/app'; import { stores } from '@sapper/app';
const { page } = stores();
export let count; export let count;
export let segment; export let segment;

View File

@@ -7,12 +7,14 @@
</script> </script>
<script> <script>
import { preloading } from '@sapper/app'; import { stores } from '@sapper/app';
import { setContext } from 'svelte'; import { setContext } from 'svelte';
export let child; export let child;
export let rootPreloadFunctionRan; export let rootPreloadFunctionRan;
const { preloading } = stores();
setContext('x', { rootPreloadFunctionRan }); setContext('x', { rootPreloadFunctionRan });
</script> </script>

View File

@@ -1,5 +1,6 @@
<script> <script>
import { page } from '@sapper/app'; import { stores } from '@sapper/app';
const { page } = stores();
</script> </script>
<h1>{$page.params.slug}</h1> <h1>{$page.params.slug}</h1>

View File

@@ -5,8 +5,8 @@
</script> </script>
<script> <script>
import { getSession } from '@sapper/app'; import { stores } from '@sapper/app';
const session = getSession(); const { session } = stores();
export let title; export let title;
</script> </script>

View File

@@ -1,6 +1,6 @@
<script> <script>
import { getSession } from '@sapper/app'; import { stores } from '@sapper/app';
const session = getSession(); const { session } = stores();
</script> </script>
<h1>{$session.title}</h1> <h1>{$session.title}</h1>

View File

@@ -1 +1,2 @@
<h1>Great success!</h1> <h1>Great success!</h1>
<a href="redirect-from">redirect from</a>

View File

@@ -0,0 +1,7 @@
<script context="module">
export function preload() {
this.redirect(301, 'redirect-to');
}
</script>
<h1>unredirected</h1>

View File

@@ -0,0 +1 @@
<h1>redirected</h1>

View File

@@ -3,6 +3,8 @@ import * as puppeteer from 'puppeteer';
import * as api from '../../../api'; import * as api from '../../../api';
import { walk } from '../../utils'; import { walk } from '../../utils';
import { AppRunner } from '../AppRunner'; import { AppRunner } from '../AppRunner';
import { wait } from '../../utils';
describe('with-basepath', function() { describe('with-basepath', function() {
this.timeout(10000); this.timeout(10000);
@@ -11,6 +13,11 @@ describe('with-basepath', function() {
let page: puppeteer.Page; let page: puppeteer.Page;
let base: string; let base: string;
// helpers
let start: () => Promise<void>;
let prefetchRoutes: () => Promise<void>;
let title: () => Promise<string>;
// hooks // hooks
before(async () => { before(async () => {
await api.build({ cwd: __dirname }); await api.build({ cwd: __dirname });
@@ -21,7 +28,7 @@ describe('with-basepath', function() {
}); });
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js'); runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page } = await runner.start()); ({ base, start, page, prefetchRoutes, title } = await runner.start());
}); });
after(() => runner.end()); after(() => runner.end());
@@ -56,8 +63,43 @@ describe('with-basepath', function() {
assert.deepEqual(non_client_assets, [ assert.deepEqual(non_client_assets, [
'custom-basepath/global.css', 'custom-basepath/global.css',
'custom-basepath/index.html', 'custom-basepath/index.html',
'custom-basepath/redirect-from/index.html',
'custom-basepath/redirect-to/index.html',
'custom-basepath/service-worker-index.html', 'custom-basepath/service-worker-index.html',
'custom-basepath/service-worker.js' 'custom-basepath/service-worker.js'
]); ]);
}); });
it('redirects on server', async () => {
await page.goto(`${base}/custom-basepath/redirect-from`);
assert.equal(
page.url(),
`${base}/custom-basepath/redirect-to`
);
assert.equal(
await title(),
'redirected'
);
});
it('redirects in client', async () => {
await page.goto(`${base}/custom-basepath`);
await start();
await prefetchRoutes();
await page.click('[href="redirect-from"]');
await wait(50);
assert.equal(
page.url(),
`${base}/custom-basepath/redirect-to`
);
assert.equal(
await title(),
'redirected'
);
});
}); });

View File

@@ -6,10 +6,10 @@ describe('manifest_data', () => {
it('creates routes', () => { it('creates routes', () => {
const { components, pages, server_routes } = create_manifest_data(path.join(__dirname, 'samples/basic')); const { components, pages, server_routes } = create_manifest_data(path.join(__dirname, 'samples/basic'));
const index = { name: 'index', file: 'index.html' }; const index = { name: 'index', file: 'index.html', has_preload: false };
const about = { name: 'about', file: 'about.html' }; const about = { name: 'about', file: 'about.html', has_preload: false };
const blog = { name: 'blog', file: 'blog/index.html' }; const blog = { name: 'blog', file: 'blog/index.html', has_preload: false };
const blog_$slug = { name: 'blog_$slug', file: 'blog/[slug].html' }; const blog_$slug = { name: 'blog_$slug', file: 'blog/[slug].html', has_preload: false };
assert.deepEqual(components, [ assert.deepEqual(components, [
index, index,
@@ -36,7 +36,6 @@ describe('manifest_data', () => {
{ {
pattern: /^\/blog\/?$/, pattern: /^\/blog\/?$/,
parts: [ parts: [
null,
{ component: blog, params: [] } { component: blog, params: [] }
] ]
}, },
@@ -73,7 +72,7 @@ describe('manifest_data', () => {
// had to remove ? and " because windows // had to remove ? and " because windows
// const quote = { name: '$34', file: '".html' }; // const quote = { name: '$34', file: '".html' };
const hash = { name: '$35', file: '#.html' }; const hash = { name: '$35', has_preload: false, file: '#.html' };
// const question_mark = { name: '$63', file: '?.html' }; // const question_mark = { name: '$63', file: '?.html' };
assert.deepEqual(components, [ assert.deepEqual(components, [
@@ -89,15 +88,16 @@ describe('manifest_data', () => {
]); ]);
}); });
it('allows regex qualifiers', () => { // this test broken
const { pages } = create_manifest_data(path.join(__dirname, 'samples/qualifiers')); // it('allows regex qualifiers', () => {
// const { pages } = create_manifest_data(path.join(__dirname, 'samples/qualifiers'));
assert.deepEqual(pages.map(p => p.pattern), [ //
/^\/([0-9-a-z]{3,})\/?$/, // assert.deepEqual(pages.map(p => p.pattern), [
/^\/([a-z]{2})\/?$/, // /^\/([0-9-a-z]{3,})\/?$/,
/^\/([^\/]+?)\/?$/ // /^\/([a-z]{2})\/?$/,
]); // /^\/([^\/]+?)\/?$/
}); // ]);
// });
it('sorts routes correctly', () => { it('sorts routes correctly', () => {
const { pages } = create_manifest_data(path.join(__dirname, 'samples/sorting')); const { pages } = create_manifest_data(path.join(__dirname, 'samples/sorting'));
@@ -105,13 +105,18 @@ describe('manifest_data', () => {
assert.deepEqual(pages.map(p => p.parts.map(part => part && part.component.file)), [ assert.deepEqual(pages.map(p => p.parts.map(part => part && part.component.file)), [
['index.html'], ['index.html'],
['about.html'], ['about.html'],
[null, 'post/index.html'], ['post/index.html'],
[null, 'post/bar.html'], [null, 'post/bar.html'],
[null, 'post/foo.html'], [null, 'post/foo.html'],
[null, 'post/f[xx].html'], [null, 'post/f[xx].html'],
[null, 'post/[id([0-9-a-z]{3,})].html'], [null, 'post/[id([0-9-a-z]{3,})].html'],
[null, 'post/[id].html'], [null, 'post/[id].html'],
['[wildcard].html'] ['[wildcard].html'],
[null, null, null, '[...spread]/deep/[...deep_spread]/xyz.html'],
[null, null, '[...spread]/deep/[...deep_spread]/index.html'],
[null, '[...spread]/deep/index.html'],
[null, '[...spread]/abc.html'],
['[...spread]/index.html']
]); ]);
}); });