Support Svelte 3

fixes #546, #551, #552, #554
This commit is contained in:
Rich Harris
2019-02-03 14:29:47 -05:00
committed by GitHub
parent 83c8d7f855
commit ca034d0857
139 changed files with 1946 additions and 2016 deletions

3
.gitignore vendored
View File

@@ -12,4 +12,5 @@ sapper
runtime.js
dist
!rollup.config.js
templates/*.js
/runtime/app.mjs
/runtime/server.mjs

View File

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

1528
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "sapper",
"version": "0.25.0",
"version": "0.26.0-alpha.4",
"description": "Military-grade apps, engineered by Svelte",
"bin": {
"sapper": "./sapper"
@@ -11,61 +11,64 @@
"config",
"sapper",
"dist/*.js",
"templates/*.js"
"runtime/*.mjs",
"runtime/internal"
],
"directories": {
"test": "test"
},
"dependencies": {
"html-minifier": "^3.5.20",
"shimport": "0.0.11",
"source-map-support": "^0.5.9",
"sourcemap-codec": "^1.4.3",
"html-minifier": "^3.5.21",
"shimport": "0.0.14",
"source-map-support": "^0.5.10",
"sourcemap-codec": "^1.4.4",
"string-hash": "^1.1.3",
"tslib": "^1.9.3"
},
"devDependencies": {
"@types/mkdirp": "^0.5.2",
"@types/mocha": "^5.2.5",
"@types/node": "^10.12.0",
"@types/puppeteer": "^1.9.0",
"@types/node": "^10.12.21",
"@types/puppeteer": "^1.11.3",
"@types/rimraf": "^2.0.2",
"agadoo": "^1.0.1",
"cheap-watch": "^1.0.0",
"cheap-watch": "^1.0.2",
"cookie": "^0.3.1",
"devalue": "^1.0.4",
"eslint": "^5.7.0",
"eslint-plugin-import": "^2.14.0",
"kleur": "^2.0.2",
"devalue": "^1.1.0",
"eslint": "^5.12.1",
"eslint-plugin-import": "^2.16.0",
"kleur": "^3.0.1",
"mkdirp": "^0.5.1",
"mocha": "^5.2.0",
"node-fetch": "^2.2.0",
"node-fetch": "^2.3.0",
"npm-run-all": "^4.1.5",
"polka": "^0.5.1",
"port-authority": "^1.0.5",
"pretty-bytes": "^5.1.0",
"puppeteer": "^1.9.0",
"puppeteer": "^1.12.0",
"require-relative": "^0.8.7",
"rimraf": "^2.6.2",
"rollup": "^0.66.6",
"rimraf": "^2.6.3",
"rollup": "^1.1.2",
"rollup-plugin-commonjs": "^9.2.0",
"rollup-plugin-json": "^3.1.0",
"rollup-plugin-node-resolve": "^3.4.0",
"rollup-plugin-node-resolve": "^4.0.0",
"rollup-plugin-replace": "^2.1.0",
"rollup-plugin-string": "^2.0.2",
"rollup-plugin-svelte": "^4.3.2",
"rollup-plugin-typescript": "^1.0.0",
"sade": "^1.4.1",
"rollup-plugin-sucrase": "^2.1.0",
"rollup-plugin-svelte": "^5.0.1",
"sade": "^1.4.2",
"sander": "^0.6.0",
"sirv": "^0.2.2",
"svelte": "^2.13.5",
"svelte-loader": "^2.11.0",
"ts-node": "^7.0.1",
"typescript": "^3.1.3",
"webpack": "^4.20.2",
"webpack-format-messages": "^2.0.3",
"sucrase": "^3.9.5",
"svelte": "^3.0.0-alpha27",
"svelte-loader": "^2.12.0",
"webpack": "^4.29.0",
"webpack-format-messages": "^2.0.5",
"yootils": "0.0.14"
},
"peerDependencies": {
"svelte": "^3.0.0"
},
"scripts": {
"test": "mocha --opts mocha.opts",
"pretest": "npm run build",
@@ -73,7 +76,7 @@
"prepare": "npm run build",
"dev": "rollup -cw",
"prepublishOnly": "npm test",
"update_mime_types": "curl http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types | grep -e \"^[^#]\" > templates/src/server/middleware/mime-types.md"
"update_mime_types": "curl http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types | grep -e \"^[^#]\" > runtime/src/server/middleware/mime-types.md"
},
"repository": "https://github.com/sveltejs/sapper",
"keywords": [

View File

@@ -1,4 +1,4 @@
import typescript from 'rollup-plugin-typescript';
import sucrase from 'rollup-plugin-sucrase';
import string from 'rollup-plugin-string';
import json from 'rollup-plugin-json';
import resolve from 'rollup-plugin-node-resolve';
@@ -12,31 +12,33 @@ const external = [].concat(
'sapper/core.js'
);
function template(kind, external, target) {
function template(kind, external) {
return {
input: `templates/src/${kind}/index.ts`,
input: `runtime/src/${kind}/index.ts`,
output: {
file: `templates/${kind}.js`,
format: 'es'
file: `runtime/${kind}.mjs`,
format: 'es',
paths: id => id.replace('@sapper', '.')
},
external,
plugins: [
resolve(),
resolve({
extensions: ['.mjs', '.js', '.ts']
}),
commonjs(),
string({
include: '**/*.md'
}),
typescript({
typescript: require('typescript'),
target
sucrase({
transforms: ['typescript']
})
]
};
}
export default [
template('client', ['__ROOT__', '__ERROR__'], 'ES2017'),
template('server', builtinModules, 'ES2015'),
template('app', id => /^(svelte\/?|@sapper\/)/.test(id)),
template('server', id => builtinModules.includes(id)),
{
input: [
@@ -54,12 +56,13 @@ export default [
external,
plugins: [
json(),
resolve(),
resolve({
extensions: ['.mjs', '.js', '.ts']
}),
commonjs(),
typescript({
typescript: require('typescript')
sucrase({
transforms: ['typescript']
})
],
experimentalCodeSplitting: true
]
}
];

View File

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

@@ -0,0 +1 @@
<svelte:component this={child.component} {...child.props}/>

View File

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

341
runtime/src/app/app.ts Normal file
View File

@@ -0,0 +1,341 @@
import { writable } from 'svelte/store.mjs';
import Sapper from '@sapper/internal/Sapper.html';
import { stores } from '@sapper/internal/shared';
import { Root, root_preload, ErrorComponent, ignore, components, routes } from '@sapper/internal/manifest-client';
import {
Target,
ScrollPosition,
Component,
Redirect,
ComponentLoader,
ComponentConstructor,
Route,
Page
} from './types';
import goto from './goto';
declare const __SAPPER__;
export const initial_data = typeof __SAPPER__ !== 'undefined' && __SAPPER__;
let ready = false;
let root_component: Component;
let current_token: {};
let root_preloaded: Promise<any>;
let current_branch = [];
const session = writable(initial_data && initial_data.session);
let $session;
let session_dirty: boolean;
session.subscribe(async value => {
$session = value;
if (!ready) return;
session_dirty = true;
const target = select_target(new URL(location.href));
const token = current_token = {};
const { redirect, props, branch } = await hydrate_target(target);
if (token !== current_token) return; // a secondary navigation happened while we were loading
await render(redirect, branch, props, target.page);
});
export let prefetching: {
href: string;
promise: Promise<{ redirect?: Redirect, data?: any }>;
} = null;
export function set_prefetching(href, promise) {
prefetching = { href, promise };
}
export let store;
export function set_store(fn) {
store = fn(initial_data.store);
}
export let target: Node;
export function set_target(element) {
target = element;
}
export let uid = 1;
export function set_uid(n) {
uid = n;
}
export let cid: number;
export function set_cid(n) {
cid = n;
}
const _history = typeof history !== 'undefined' ? history : {
pushState: (state: any, title: string, href: string) => {},
replaceState: (state: any, title: string, href: string) => {},
scrollRestoration: ''
};
export { _history as history };
export const scroll_history: Record<string, ScrollPosition> = {};
export function select_target(url: URL): Target {
if (url.origin !== location.origin) return null;
if (!url.pathname.startsWith(initial_data.baseUrl)) return null;
const path = url.pathname.slice(initial_data.baseUrl.length);
// avoid accidental clashes between server routes and page routes
if (ignore.some(pattern => pattern.test(path))) return;
for (let i = 0; i < routes.length; i += 1) {
const route = routes[i];
const match = route.pattern.exec(path);
if (match) {
const query: Record<string, string | string[]> = Object.create(null);
if (url.search.length > 0) {
url.search.slice(1).split('&').forEach(searchParam => {
let [, key, value] = /([^=]*)(?:=(.*))?/.exec(decodeURIComponent(searchParam));
value = (value || '').replace(/\+/g, ' ');
if (typeof query[key] === 'string') query[key] = [<string>query[key]];
if (typeof query[key] === 'object') (query[key] as string[]).push(value);
else query[key] = value;
});
}
const part = route.parts[route.parts.length - 1];
const params = part.params ? part.params(match) : {};
const page = { path, query, params };
return { href: url.href, route, match, page };
}
}
}
export function scroll_state() {
return {
x: pageXOffset,
y: pageYOffset
};
}
export async function navigate(target: Target, id: number, noscroll?: boolean, hash?: string): Promise<any> {
if (id) {
// popstate or initial navigation
cid = id;
} else {
const current_scroll = scroll_state();
// clicked on a link. preserve scroll state
scroll_history[cid] = current_scroll;
id = cid = ++uid;
scroll_history[cid] = noscroll ? current_scroll : { x: 0, y: 0 };
}
cid = id;
if (root_component) stores.preloading.set(true);
const loaded = prefetching && prefetching.href === target.href ?
prefetching.promise :
hydrate_target(target);
prefetching = null;
const token = current_token = {};
const { redirect, props, branch } = await loaded;
if (token !== current_token) return; // a secondary navigation happened while we were loading
await render(redirect, branch, props, target.page);
if (document.activeElement) document.activeElement.blur();
if (!noscroll) {
let scroll = scroll_history[id];
if (hash) {
// scroll is an element id (from a hash), we need to compute y.
const deep_linked = document.querySelector(hash);
if (deep_linked) {
scroll = {
x: 0,
y: deep_linked.getBoundingClientRect().top
};
}
}
scroll_history[cid] = scroll;
if (scroll) scrollTo(scroll.x, scroll.y);
}
}
async function render(redirect: Redirect, branch: any[], props: any, page: Page) {
if (redirect) return goto(redirect.location, { replaceState: true });
stores.page.set(page);
stores.preloading.set(false);
if (root_component) {
root_component.props = props;
} else {
// first load — remove SSR'd <head> contents
const start = document.querySelector('#sapper-head-start');
const end = document.querySelector('#sapper-head-end');
if (start && end) {
while (start.nextSibling !== end) detach(start.nextSibling);
detach(start);
detach(end);
}
root_component = new Sapper({
target,
props: {
Root,
props,
session
},
hydrate: true
});
}
current_branch = branch;
ready = true;
session_dirty = false;
}
export async function hydrate_target(target: Target): Promise<{
redirect?: Redirect;
props?: any;
branch?: Array<{ Component: ComponentConstructor, preload: (page) => Promise<any>, segment: string }>
}> {
const { route, page } = target;
const segments = page.path.split('/').filter(Boolean);
let redirect: Redirect = null;
let error: { statusCode: number, message: Error | string } = null;
const preload_context = {
fetch: (url: string, opts?: any) => fetch(url, opts),
redirect: (statusCode: number, location: string) => {
if (redirect && (redirect.statusCode !== statusCode || redirect.location !== location)) {
throw new Error(`Conflicting redirects`);
}
redirect = { statusCode, location };
},
error: (statusCode: number, message: Error | string) => {
error = { statusCode, message };
}
};
if (!root_preloaded) {
root_preloaded = initial_data.preloaded[0] || root_preload.call(preload_context, {
path: page.path,
query: page.query,
params: {}
}, $session);
}
let branch;
try {
branch = await Promise.all(route.parts.map(async (part, i) => {
if (!part) return null;
const segment = segments[i];
if (!session_dirty && current_branch[i] && current_branch[i].segment === segment) return current_branch[i];
const { default: Component, preload } = await load_component(components[part.i]);
let preloaded;
if (ready || !initial_data.preloaded[i + 1]) {
preloaded = preload
? await preload.call(preload_context, {
path: page.path,
query: page.query,
params: part.params ? part.params(target.match) : {}
}, $session)
: {};
} else {
preloaded = initial_data.preloaded[i + 1];
}
return { Component, preloaded, segment };
}));
} catch (e) {
error = { statusCode: 500, message: e };
branch = [];
}
if (redirect) return { redirect };
if (error) {
// TODO be nice if this was less of a special case
return {
props: {
child: {
component: ErrorComponent,
props: {
error: typeof error.message === 'string' ? new Error(error.message) : error.message,
status: error.statusCode
}
}
},
branch
};
}
const props = Object.assign({}, await root_preloaded, {
child: { segment: segments[0] }
});
let level = props.child;
branch.forEach((node, i) => {
if (!node) return;
level.component = node.Component;
level.props = Object.assign({}, node.preloaded, {
child: { segment: segments[i + 1] }
});
level = level.props.child;
});
return { props, branch };
}
function load_css(chunk: string) {
const href = `client/${chunk}`;
if (document.querySelector(`link[href="${href}"]`)) return;
return new Promise((fulfil, reject) => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = href;
link.onload = () => fulfil();
link.onerror = reject;
document.head.appendChild(link);
});
}
export function load_component(component: ComponentLoader): Promise<{
default: ComponentConstructor,
preload?: (input: any) => any
}> {
// TODO this is temporary — once placeholders are
// always rewritten, scratch the ternary
const promises: Array<Promise<any>> = (typeof component.css === 'string' ? [] : component.css.map(load_css));
promises.unshift(component.js());
return Promise.all(promises).then(values => values[0]);
}
function detach(node: Node) {
node.parentNode.removeChild(node);
}

View File

@@ -1,7 +1,7 @@
import { history, select_route, navigate, cid } from '../app';
import { history, select_target, navigate, cid } from '../app';
export default function goto(href: string, opts = { replaceState: false }) {
const target = select_route(new URL(href, document.baseURI));
const target = select_target(new URL(href, document.baseURI));
if (target) {
history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);

12
runtime/src/app/index.ts Normal file
View File

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

View File

@@ -0,0 +1,14 @@
import { select_target, prefetching, set_prefetching, hydrate_target } from '../app';
import { Target } from '../types';
export default function prefetch(href: string) {
const target: Target = select_target(new URL(href, document.baseURI));
if (target) {
if (!prefetching || href !== prefetching.href) {
set_prefetching(href, hydrate_target(target));
}
return prefetching.promise;
}
}

View File

@@ -1,11 +1,12 @@
import { components, pages, load_component } from "../app";
import { components, routes } from '@sapper/internal/manifest-client';
import { load_component } from '../app';
export default function prefetchRoutes(pathnames: string[]) {
return pages
.filter(route => {
if (!pathnames) return true;
return pathnames.some(pathname => route.pattern.test(pathname));
})
return routes
.filter(pathnames
? route => pathnames.some(pathname => route.pattern.test(pathname))
: () => true
)
.reduce((promise: Promise<any>, route) => promise.then(() => {
return Promise.all(route.parts.map(part => part && load_component(components[part.i])));
}), Promise.resolve());

View File

@@ -5,26 +5,22 @@ import {
navigate,
scroll_history,
scroll_state,
select_route,
set_store,
select_target,
set_target,
uid,
set_uid,
set_cid
} from '../app';
import prefetch from '../prefetch/index';
import { Store, ScrollPosition } from '../types';
export default function start(opts: {
target: Node,
store?: (data: any) => Store
target: Node
}) {
if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual';
}
set_target(opts.target);
if (opts.store) set_store(opts.store);
addEventListener('click', handle_click);
addEventListener('popstate', handle_popstate);
@@ -39,7 +35,7 @@ export default function start(opts: {
history.replaceState({ id: uid }, '', href);
if (!initial_data.error) {
const target = select_route(new URL(location.href));
const target = select_target(new URL(location.href));
if (target) return navigate(target, uid, false, hash);
}
});
@@ -96,7 +92,7 @@ function handle_click(event: MouseEvent) {
// Don't handle hash changes
if (url.pathname === location.pathname && url.search === location.search) return;
const target = select_route(url);
const target = select_target(url);
if (target) {
const noscroll = a.hasAttribute('sapper-noscroll');
navigate(target, null, noscroll, url.hash);
@@ -119,7 +115,7 @@ function handle_popstate(event: PopStateEvent) {
if (event.state) {
const url = new URL(location.href);
const target = select_route(url);
const target = select_target(url);
if (target) {
navigate(target, event.state.id);
} else {

View File

@@ -8,21 +8,14 @@ type Child = {
component?: Component;
};
export type RootProps = {
path: string;
params: Record<string, string>;
query: Record<string, string>;
child: Child;
};
export interface ComponentConstructor {
new (options: { target: Node, data: any, store: Store, hydrate: boolean }): Component;
new (options: { target: Node, props: any, hydrate: boolean }): Component;
preload: (props: { params: Params, query: Query }) => Promise<any>;
};
export interface Component {
set: (data: any) => void;
destroy: () => void;
$set: (data: any) => void;
$destroy: () => void;
}
export type ComponentLoader = {
@@ -30,7 +23,7 @@ export type ComponentLoader = {
css: string[]
};
export type Page = {
export type Route = {
pattern: RegExp;
parts: Array<{
i: number;
@@ -42,7 +35,7 @@ export type Manifest = {
ignore: RegExp[];
root: ComponentConstructor;
error: () => Promise<{ default: ComponentConstructor }>;
pages: Page[]
pages: Route[]
};
export type ScrollPosition = {
@@ -51,11 +44,10 @@ export type ScrollPosition = {
};
export type Target = {
url: URL;
path: string;
page: Page;
href: string;
route: Route;
match: RegExpExecArray;
query: Record<string, string | string[]>;
page: Page;
};
export type Redirect = {
@@ -63,6 +55,8 @@ export type Redirect = {
location: string;
};
export type Store = {
get: () => any;
}
export type Page = {
path: string;
params: Record<string, string>;
query: Record<string, string | string[]>;
};

View File

@@ -0,0 +1 @@
export const IGNORE = '__SAPPER__IGNORE__';

View File

@@ -1,15 +1,19 @@
import * as fs from 'fs';
import * as path from 'path';
import { writable } from 'svelte/store.mjs';
import fs from 'fs';
import path from 'path';
import cookie from 'cookie';
import devalue from 'devalue';
import fetch from 'node-fetch';
import { URL, resolve } from 'url';
import { build_dir, dev, src_dir, IGNORE } from '../placeholders';
import { Manifest, Page, Props, Req, Res, Store } from './types';
import URL from 'url';
import { IGNORE } from '../constants';
import { Manifest, Page, Props, Req, Res } from './types';
import { build_dir, dev, src_dir } from '@sapper/internal/manifest-server';
import { stores } from '@sapper/internal/shared';
import Sapper from '@sapper/internal/Sapper.html';
export function get_page_handler(
manifest: Manifest,
store_getter: (req: Req, res: Res) => Store
session_getter: (req: Req, res: Res) => any
) {
const get_build_info = dev
? () => JSON.parse(fs.readFileSync(path.join(build_dir, 'build.json'), 'utf-8'))
@@ -77,7 +81,7 @@ export function get_page_handler(
res.setHeader('Link', link);
}
const store = store_getter ? store_getter(req, res) : null;
const session = session_getter(req, res);
let redirect: { statusCode: number, location: string };
let preload_error: { statusCode: number, message: Error | string };
@@ -94,7 +98,7 @@ export function get_page_handler(
preload_error = { statusCode, message };
},
fetch: (url: string, opts?: any) => {
const parsed = new URL(url, `http://127.0.0.1:${process.env.PORT}${req.baseUrl ? req.baseUrl + '/' :''}`);
const parsed = new URL.URL(url, `http://127.0.0.1:${process.env.PORT}${req.baseUrl ? req.baseUrl + '/' :''}`);
if (opts) {
opts = Object.assign({}, opts);
@@ -128,16 +132,16 @@ export function get_page_handler(
}
return fetch(parsed.href, opts);
},
store
}
};
let preloaded;
let match;
let params;
try {
const root_preloaded = manifest.root.preload
? manifest.root.preload.call(preload_context, {
const root_preloaded = manifest.root_preload
? manifest.root_preload.call(preload_context, {
path: req.path,
query: req.query,
params: {}
@@ -146,17 +150,21 @@ export function get_page_handler(
match = error ? null : page.pattern.exec(req.path);
let toPreload = [root_preloaded];
if (!isSWIndexHtml) {
toPreload = toPreload.concat(page.parts.map(part => {
if (!part) return null;
return part.component.preload
? part.component.preload.call(preload_context, {
// the deepest level is used below, to initialise the store
params = part.params ? part.params(match) : {};
return part.preload
? part.preload.call(preload_context, {
path: req.path,
query: req.query,
params: part.params ? part.params(match) : {}
})
params
}, session)
: {};
}))
}
@@ -169,7 +177,7 @@ export function get_page_handler(
try {
if (redirect) {
const location = resolve(req.baseUrl || '/', redirect.location);
const location = URL.resolve(req.baseUrl || '/', redirect.location);
res.statusCode = redirect.statusCode;
res.setHeader('Location', location);
@@ -183,67 +191,63 @@ export function get_page_handler(
return;
}
const serialized = {
preloaded: `[${preloaded.map(data => try_serialize(data)).join(',')}]`,
store: store && try_serialize(store.get())
};
const segments = req.path.split('/').filter(Boolean);
const props: Props = {
path: req.path,
query: req.query,
params: {},
child: null
};
if (error) {
props.error = error instanceof Error ? error : { message: error };
props.status = status;
}
const data = Object.assign({}, props, preloaded[0], {
params: {},
const props = Object.assign({}, preloaded[0], {
child: {
segment: segments[0]
segment: segments[0],
props: {}
}
});
let level = data.child;
if (isSWIndexHtml) {
level.props = Object.assign({}, props, {
params: {}
})
} else {
let level = props.child;
if (!isSWIndexHtml) {
for (let i = 0; i < page.parts.length; i += 1) {
const part = page.parts[i];
if (!part) continue;
const get_params = part.params || (() => ({}));
Object.assign(level, {
component: part.component,
props: Object.assign({}, props, {
params: get_params(match)
}, preloaded[i + 1])
props: Object.assign({}, preloaded[i + 1])
});
level.props.child = <Props["child"]>{
segment: segments[i + 1]
segment: segments[i + 1],
props: {}
};
level = level.props.child;
}
}
const { html, head, css } = manifest.root.render(data, {
store
if (error) {
props.child.props.error = error instanceof Error ? error : { message: error };
props.child.props.status = status;
}
stores.page.set({
path: req.path,
query: req.query,
params: params
});
const { html, head, css } = Sapper.render({
Root: manifest.root,
props: props,
session: writable(session)
});
const serialized = {
preloaded: `[${preloaded.map(data => try_serialize(data)).join(',')}]`,
session: session && try_serialize(session, err => {
throw new Error(`Failed to serialize session data: ${err.message}`);
})
};
let script = `__SAPPER__={${[
error && `error:1`,
`baseUrl:"${req.baseUrl}"`,
serialized.preloaded && `preloaded:${serialized.preloaded}`,
serialized.store && `store:${serialized.store}`
serialized.session && `session:${serialized.session}`
].filter(Boolean).join(',')}};`;
if (has_service_worker) {
@@ -302,6 +306,7 @@ export function get_page_handler(
res.statusCode = status;
res.end(body);
} catch(err) {
console.log(err);
if (error) {
// we encountered an error while rendering the error page — oops
res.statusCode = 500;
@@ -338,10 +343,11 @@ function read_template(dir = build_dir) {
return fs.readFileSync(`${dir}/template.html`, 'utf-8');
}
function try_serialize(data: any) {
function try_serialize(data: any, fail?: (err) => void) {
try {
return devalue(data);
} catch (err) {
if (fail) fail(err);
return null;
}
}

View File

@@ -1,4 +1,4 @@
import { IGNORE } from '../placeholders';
import { IGNORE } from '../constants';
import { Req, Res, ServerRoute } from './types';
export function get_server_route_handler(routes: ServerRoute[]) {

View File

@@ -1,16 +1,17 @@
import * as fs from 'fs';
import * as path from 'path';
import { build_dir, dev, manifest, IGNORE } from '../placeholders';
import { Handler, Req, Res, Store } from './types';
import fs from 'fs';
import path from 'path';
import { build_dir, dev, manifest } from '@sapper/internal/manifest-server';
import { Handler, Req, Res } from './types';
import { get_server_route_handler } from './get_server_route_handler';
import { get_page_handler } from './get_page_handler';
import { lookup } from './mime';
import { IGNORE } from '../constants';
export default function middleware(opts: {
store?: (req: Req, res: Res) => Store,
session?: (req: Req, res: Res) => any,
ignore?: any
} = {}) {
const { store, ignore } = opts;
const { session, ignore } = opts;
let emitted_basepath = false;
@@ -73,7 +74,7 @@ export default function middleware(opts: {
get_server_route_handler(manifest.server_routes),
get_page_handler(manifest, store)
get_page_handler(manifest, session || noop)
].filter(Boolean));
}
@@ -140,4 +141,6 @@ export function serve({ prefix, pathname, cache_control }: {
next();
}
};
}
}
function noop(){}

View File

@@ -12,6 +12,7 @@ export type Page = {
name: string;
component: Component;
params?: (match: RegExpMatchArray) => Record<string, string>;
preload?: (data: any) => any | Promise<any>;
}>
};
@@ -19,19 +20,13 @@ export type Manifest = {
server_routes: ServerRoute[];
pages: Page[];
root: Component;
root_preload?: (data: any) => any | Promise<any>;
error: Component;
}
export type Handler = (req: Req, res: Res, next: () => void) => void;
export type Store = {
get: () => any
};
export type Props = {
path: string;
query: Record<string, string>;
params: Record<string, string>;
error?: { message: string };
status?: number;
child: {
@@ -60,10 +55,9 @@ export interface Res extends ServerResponse {
export { ServerResponse };
interface Component {
render: (data: any, opts: { store: Store }) => {
render: (data: any) => {
head: string;
css: { code: string, map: any };
html: string
},
preload: (data: any) => any | Promise<any>
}
}

View File

@@ -9,6 +9,7 @@ import read_template from '../core/read_template';
import { CompileResult } from '../core/create_compilers/interfaces';
import { noop } from './utils/noop';
import validate_bundler from './utils/validate_bundler';
import { copy_runtime } from './utils/copy_runtime';
type Opts = {
cwd?: string;
@@ -26,7 +27,7 @@ export async function build({
cwd,
src = 'src',
routes = 'src/routes',
output = '__sapper__',
output = 'src/node_modules/@sapper',
static: static_files = 'static',
dest = '__sapper__/build',
@@ -47,6 +48,10 @@ export async function build({
throw new Error(`Legacy builds are not supported for projects using webpack`);
}
rimraf.sync(path.join(output, '**/*'));
mkdirp.sync(output);
copy_runtime(output);
rimraf.sync(path.join(dest, '**/*'));
mkdirp.sync(`${dest}/client`);
copy_shimport(dest);
@@ -66,7 +71,7 @@ export async function build({
const manifest_data = create_manifest_data(routes);
// create src/manifest/client.js and src/manifest/server.js
// create src/node_modules/@sapper/app.mjs and server.mjs
create_main_manifests({
bundler,
manifest_data,

View File

@@ -15,6 +15,7 @@ import { copy_shimport } from './utils/copy_shimport';
import { ManifestData, FatalEvent, ErrorEvent, ReadyEvent, InvalidEvent } from '../interfaces';
import read_template from '../core/read_template';
import { noop } from './utils/noop';
import { copy_runtime } from './utils/copy_runtime';
type Opts = {
cwd?: string,
@@ -72,7 +73,7 @@ class Watcher extends EventEmitter {
cwd = '.',
src = 'src',
routes = 'src/routes',
output = '__sapper__',
output = 'src/node_modules/@sapper',
static: static_files = 'static',
dest = '__sapper__/dev',
'dev-port': dev_port,
@@ -144,6 +145,11 @@ class Watcher extends EventEmitter {
}
const { cwd, src, dest, routes, output, static: static_files } = this.dirs;
rimraf.sync(path.join(output, '**/*'));
mkdirp.sync(output);
copy_runtime(output);
rimraf.sync(dest);
mkdirp.sync(`${dest}/client`);
if (this.bundler === 'rollup') copy_shimport(dest);
@@ -484,7 +490,7 @@ function watch_dir(
let watch: any;
let closed = false;
import('cheap-watch').then(CheapWatch => {
import('cheap-watch').then(({ default: CheapWatch }) => {
if (closed) return;
watch = new CheapWatch({ dir, filter, debounce: 50 });

View File

@@ -0,0 +1,21 @@
import * as fs from 'fs';
import * as path from 'path';
import mkdirp from 'mkdirp';
const runtime = [
'app.mjs',
'server.mjs',
'internal/shared.mjs',
'internal/Sapper.html',
'internal/layout.html'
].map(file => ({
file,
source: fs.readFileSync(path.join(__dirname, `../runtime/${file}`), 'utf-8')
}));
export function copy_runtime(output: string) {
runtime.forEach(({ file, source }) => {
mkdirp.sync(path.dirname(`${output}/${file}`));
fs.writeFileSync(`${output}/${file}`, source);
});
}

View File

@@ -10,7 +10,7 @@ const prog = sade('sapper').version(pkg.version);
if (process.argv[2] === 'start') {
// remove this in a future version
console.error(colors.bold.red(`'sapper start' has been removed`));
console.error(colors.bold().red(`'sapper start' has been removed`));
console.error(`Use 'node [build_dir]' instead`);
process.exit(1);
}
@@ -29,7 +29,7 @@ prog.command('dev')
.option('--src', 'Source directory', 'src')
.option('--routes', 'Routes directory', 'src/routes')
.option('--static', 'Static files directory', 'static')
.option('--output', 'Sapper output directory', '__sapper__')
.option('--output', 'Sapper output directory', 'src/node_modules/@sapper')
.option('--build-dir', 'Development build directory', '__sapper__/dev')
.action(async (opts: {
port: number,
@@ -74,7 +74,7 @@ prog.command('dev')
watcher.on('ready', async (event: ReadyEvent) => {
if (first) {
console.log(colors.bold.cyan(`> Listening on http://localhost:${event.port}`));
console.log(colors.bold().cyan(`> Listening on http://localhost:${event.port}`));
if (opts.open) {
const { exec } = await import('child_process');
exec(`open http://localhost:${event.port}`);
@@ -85,7 +85,7 @@ prog.command('dev')
watcher.on('invalid', (event: InvalidEvent) => {
const changed = event.changed.map(filename => path.relative(process.cwd(), filename)).join(', ');
console.log(`\n${colors.bold.cyan(changed)} changed. rebuilding...`);
console.log(`\n${colors.bold().cyan(changed)} changed. rebuilding...`);
});
watcher.on('error', (event: ErrorEvent) => {
@@ -94,13 +94,13 @@ prog.command('dev')
});
watcher.on('fatal', (event: FatalEvent) => {
console.log(colors.bold.red(`> ${event.message}`));
console.log(colors.bold().red(`> ${event.message}`));
if (event.log) console.log(event.log);
});
watcher.on('build', (event: BuildEvent) => {
if (event.errors.length) {
console.log(colors.bold.red(`${event.type}`));
console.log(colors.bold().red(`${event.type}`));
event.errors.filter(e => !e.duplicate).forEach(error => {
if (error.file) console.log(colors.bold(error.file));
@@ -112,7 +112,7 @@ prog.command('dev')
console.log(`${hidden} duplicate ${hidden === 1 ? 'error' : 'errors'} hidden\n`);
}
} else if (event.warnings.length) {
console.log(colors.bold.yellow(`${event.type}`));
console.log(colors.bold().yellow(`${event.type}`));
event.warnings.filter(e => !e.duplicate).forEach(warning => {
if (warning.file) console.log(colors.bold(warning.file));
@@ -124,11 +124,12 @@ prog.command('dev')
console.log(`${hidden} duplicate ${hidden === 1 ? 'warning' : 'warnings'} hidden\n`);
}
} else {
console.log(`${colors.bold.green(`${event.type}`)} ${colors.gray(`(${format_milliseconds(event.duration)})`)}`);
console.log(`${colors.bold().green(`${event.type}`)} ${colors.gray(`(${format_milliseconds(event.duration)})`)}`);
}
});
} catch (err) {
console.log(colors.bold.red(`> ${err.message}`));
console.log(colors.bold().red(`> ${err.message}`));
console.log(colors.gray(err.stack));
process.exit(1);
}
});
@@ -141,7 +142,7 @@ prog.command('build [dest]')
.option('--cwd', 'Current working directory', '.')
.option('--src', 'Source directory', 'src')
.option('--routes', 'Routes directory', 'src/routes')
.option('--output', 'Sapper output directory', '__sapper__')
.option('--output', 'Sapper output directory', 'src/node_modules/@sapper')
.example(`build custom-dir -p 4567`)
.action(async (dest = '__sapper__/build', opts: {
port: string,
@@ -168,9 +169,10 @@ prog.command('build [dest]')
require('./server/server.js');
`.replace(/^\t+/gm, '').trim());
console.error(`\n> Finished in ${elapsed(start)}. Type ${colors.bold.cyan(`node ${dest}`)} to run the app.`);
console.error(`\n> Finished in ${elapsed(start)}. Type ${colors.bold().cyan(`node ${dest}`)} to run the app.`);
} catch (err) {
console.log(`${colors.bold.red(`> ${err.message}`)}`);
console.log(`${colors.bold().red(`> ${err.message}`)}`);
console.log(colors.gray(err.stack));
process.exit(1);
}
});
@@ -186,7 +188,7 @@ prog.command('export [dest]')
.option('--src', 'Source directory', 'src')
.option('--routes', 'Routes directory', 'src/routes')
.option('--static', 'Static files directory', 'static')
.option('--output', 'Sapper output directory', '__sapper__')
.option('--output', 'Sapper output directory', 'src/node_modules/@sapper')
.option('--build-dir', 'Intermediate build directory', '__sapper__/build')
.action(async (dest = '__sapper__/export', opts: {
build: boolean,
@@ -220,24 +222,24 @@ prog.command('export [dest]')
timeout: opts.timeout,
oninfo: event => {
console.log(colors.bold.cyan(`> ${event.message}`));
console.log(colors.bold().cyan(`> ${event.message}`));
},
onfile: event => {
const size_color = event.size > 150000 ? colors.bold.red : event.size > 50000 ? colors.bold.yellow : colors.bold.gray;
const size_color = event.size > 150000 ? colors.bold().red : event.size > 50000 ? colors.bold().yellow : colors.bold().gray;
const size_label = size_color(left_pad(pb(event.size), 10));
const file_label = event.status === 200
? event.file
: colors.bold[event.status >= 400 ? 'red' : 'yellow'](`(${event.status}) ${event.file}`);
: colors.bold()[event.status >= 400 ? 'red' : 'yellow'](`(${event.status}) ${event.file}`);
console.log(`${size_label} ${file_label}`);
}
});
console.error(`\n> Finished in ${elapsed(start)}. Type ${colors.bold.cyan(`npx serve ${dest}`)} to run the app.`);
console.error(`\n> Finished in ${elapsed(start)}. Type ${colors.bold().cyan(`npx serve ${dest}`)} to run the app.`);
} catch (err) {
console.error(colors.bold.red(`> ${err.message}`));
console.error(colors.bold().red(`> ${err.message}`));
process.exit(1);
}
});

View File

@@ -47,12 +47,13 @@ export default class RollupResult implements CompileResult {
} else {
for (const name in compiler.input) {
const file = compiler.input[name];
this.assets[name] = compiler.chunks.find(chunk => file in chunk.modules).fileName;
const chunk = compiler.chunks.find(chunk => file in chunk.modules);
if (chunk) this.assets[name] = chunk.fileName;
}
}
this.summary = compiler.chunks.map(chunk => {
const size_color = chunk.code.length > 150000 ? colors.bold.red : chunk.code.length > 50000 ? colors.bold.yellow : colors.bold.white;
const size_color = chunk.code.length > 150000 ? colors.bold().red : chunk.code.length > 50000 ? colors.bold().yellow : colors.bold().white;
const size_label = left_pad(pb(chunk.code.length), 10);
const lines = [size_color(`${size_label} ${chunk.fileName}`)];

View File

@@ -153,7 +153,7 @@ export default function extract_css(client_result: CompileResult, components: Pa
chunks_with_css.add(chunk);
});
const entry = path.resolve(dirs.src, 'client.js');
const entry = path.resolve(dirs.src, 'app.mjs');
const entry_chunk = client_result.chunks.find(chunk => chunk.modules.indexOf(entry) !== -1);
const entry_chunk_dependencies: Set<Chunk> = new Set([entry_chunk]);
@@ -161,6 +161,8 @@ export default function extract_css(client_result: CompileResult, components: Pa
// recursively find the chunks this component depends on
entry_chunk_dependencies.forEach(chunk => {
if (!chunk) return; // TODO why does this happen?
chunk.imports.forEach(file => {
entry_chunk_dependencies.add(lookup.get(file));
});
@@ -182,7 +184,8 @@ export default function extract_css(client_result: CompileResult, components: Pa
if (!chunk) {
// this should never happen!
throw new Error(`Could not find chunk that owns ${component.file}`);
return;
// throw new Error(`Could not find chunk that owns ${component.file}`);
}
const chunk_dependencies: Set<Chunk> = new Set([chunk]);
@@ -190,6 +193,8 @@ export default function extract_css(client_result: CompileResult, components: Pa
// recursively find the chunks this component depends on
chunk_dependencies.forEach(chunk => {
if (!chunk) return; // TODO why does this happen?
chunk.imports.forEach(file => {
chunk_dependencies.add(lookup.get(file));
});

View File

@@ -1,5 +1,6 @@
import * as fs from 'fs';
import * as path from 'path';
import svelte from 'svelte/compiler';
import { Page, PageComponent, ServerRoute, ManifestData } from '../interfaces';
import { posixify, reserved_words } from '../utils';
@@ -9,6 +10,22 @@ export default function create_manifest_data(cwd: string): ManifestData {
throw new Error(`As of Sapper 0.21, the routes/ directory should become src/routes/`);
}
function has_preload(file: string) {
const source = fs.readFileSync(path.join(cwd, file), 'utf-8');
if (/preload/.test(source)) {
try {
const { stats } = svelte.compile(source, {
generate: false,
onwarn: () => {}
});
return !!stats.vars.find((variable: any) => variable.module && variable.export_name === 'preload');
} catch (err) {}
}
return false;
}
const components: PageComponent[] = [];
const pages: Page[] = [];
const server_routes: ServerRoute[] = [];
@@ -16,7 +33,8 @@ export default function create_manifest_data(cwd: string): ManifestData {
const default_layout: PageComponent = {
default: true,
name: '_default_layout',
file: null
file: null,
has_preload: false
};
function walk(
@@ -107,7 +125,8 @@ export default function create_manifest_data(cwd: string): ManifestData {
const component = fs.existsSync(index) && {
name: `${get_slug(item.file)}__layout`,
file: `${item.file}/_layout.html`
file: `${item.file}/_layout.html`,
has_preload: has_preload(`${item.file}/_layout.html`)
};
if (component) components.push(component);
@@ -125,7 +144,8 @@ export default function create_manifest_data(cwd: string): ManifestData {
else if (item.is_page) {
const component = {
name: get_slug(item.file),
file: item.file
file: item.file,
has_preload: has_preload(item.file)
};
const parts = stack.concat({
@@ -162,7 +182,8 @@ export default function create_manifest_data(cwd: string): ManifestData {
const root = fs.existsSync(root_file)
? {
name: 'main',
file: '_layout.html'
file: '_layout.html',
has_preload: has_preload('_layout.html')
}
: default_layout;

View File

@@ -26,17 +26,13 @@ export function create_main_manifests({
}) {
if (!fs.existsSync(output)) fs.mkdirSync(output);
const path_to_routes = path.relative(output, routes);
const path_to_routes = path.relative(`${output}/internal`, routes);
const client_manifest = generate_client(manifest_data, path_to_routes, bundler, dev, dev_port);
const server_manifest = generate_server(manifest_data, path_to_routes, cwd, src, dest, dev);
const client_manifest = generate_client_manifest(manifest_data, path_to_routes, bundler, dev, dev_port);
const server_manifest = generate_server_manifest(manifest_data, path_to_routes, cwd, src, dest, dev);
write_if_changed(
`${output}/_layout.html`,
`<svelte:component this={child.component} {...child.props}/>`
);
write_if_changed(`${output}/client.js`, client_manifest);
write_if_changed(`${output}/server.js`, server_manifest);
write_if_changed(`${output}/internal/manifest-client.mjs`, client_manifest);
write_if_changed(`${output}/internal/manifest-server.mjs`, server_manifest);
}
export function create_serviceworker_manifest({ manifest_data, output, client_files, static_files }: {
@@ -71,16 +67,13 @@ export function create_serviceworker_manifest({ manifest_data, output, client_fi
write_if_changed(`${output}/service-worker.js`, code);
}
function generate_client(
function generate_client_manifest(
manifest_data: ManifestData,
path_to_routes: string,
bundler: string,
dev: boolean,
dev_port?: number
) {
const template_file = path.resolve(__dirname, '../templates/client.js');
const template = fs.readFileSync(template_file, 'utf-8');
const page_ids = new Set(manifest_data.pages.map(page =>
page.pattern.toString()));
@@ -100,64 +93,59 @@ function generate_client(
component_indexes[component.name] = i;
return `{
js: () => import(${annotation}${stringify(source)}),
css: "__SAPPER_CSS_PLACEHOLDER:${stringify(component.file, false)}__"
}`;
}).join(',\n\t\t')}
]`.replace(/^\t/gm, '').trim();
js: () => import(${annotation}${stringify(source)}),
css: "__SAPPER_CSS_PLACEHOLDER:${stringify(component.file, false)}__"
}`;
}).join(',\n\t\t\t\t')}
]`.replace(/^\t/gm, '');
let needs_decode = false;
let pages = `[
${manifest_data.pages.map(page => `{
// ${page.parts[page.parts.length - 1].component.file}
pattern: ${page.pattern},
parts: [
${page.parts.map(part => {
if (part === null) return 'null';
let routes = `[
${manifest_data.pages.map(page => `{
// ${page.parts[page.parts.length - 1].component.file}
pattern: ${page.pattern},
parts: [
${page.parts.map(part => {
if (part === null) return 'null';
if (part.params.length > 0) {
needs_decode = true;
const props = part.params.map((param, i) => `${param}: d(match[${i + 1}])`);
return `{ i: ${component_indexes[part.component.name]}, params: match => ({ ${props.join(', ')} }) }`;
}
if (part.params.length > 0) {
needs_decode = true;
const props = part.params.map((param, i) => `${param}: d(match[${i + 1}])`);
return `{ i: ${component_indexes[part.component.name]}, params: match => ({ ${props.join(', ')} }) }`;
}
return `{ i: ${component_indexes[part.component.name]} }`;
}).join(',\n\t\t\t\t')}
]
}`).join(',\n\n\t\t')}
]`.replace(/^\t/gm, '').trim();
return `{ i: ${component_indexes[part.component.name]} }`;
}).join(',\n\t\t\t\t\t\t')}
]
}`).join(',\n\n\t\t\t\t')}
]`.replace(/^\t/gm, '');
if (needs_decode) {
pages = `(d => ${pages})(decodeURIComponent)`
routes = `(d => ${routes})(decodeURIComponent)`
}
let footer = '';
return `
// This file is generated by Sapper — do not edit it!
export { default as Root } from '${stringify(get_file(path_to_routes, manifest_data.root), false)}';
export { preload as root_preload } from '${manifest_data.root.has_preload ? stringify(get_file(path_to_routes, manifest_data.root), false) : './shared'}';
export { default as ErrorComponent } from '${stringify(posixify(`${path_to_routes}/_error.html`), false)}';
if (dev) {
const sapper_dev_client = posixify(
path.resolve(__dirname, '../sapper-dev-client.js')
);
export const ignore = [${server_routes_to_ignore.map(route => route.pattern).join(', ')}];
footer = `
export const components = ${components};
if (typeof window !== 'undefined') {
import(${stringify(sapper_dev_client)}).then(client => {
client.connect(${dev_port});
});
}`.replace(/^\t{3}/gm, '');
}
export const routes = ${routes};
return `// This file is generated by Sapper — do not edit it!\n` + template
.replace('__ROOT__', stringify(get_file(path_to_routes, manifest_data.root), false))
.replace('__ERROR__', stringify(posixify(`${path_to_routes}/_error.html`), false))
.replace('__IGNORE__', `[${server_routes_to_ignore.map(route => route.pattern).join(', ')}]`)
.replace('__COMPONENTS__', components)
.replace('__PAGES__', pages) +
footer;
${dev ? `if (typeof window !== 'undefined') {
import(${stringify(posixify(path.resolve(__dirname, '../sapper-dev-client.js')))}).then(client => {
client.connect(${dev_port});
});
}` : ''}
`.replace(/^\t{2}/gm, '').trim();
}
function generate_server(
function generate_server_manifest(
manifest_data: ManifestData,
path_to_routes: string,
cwd: string,
@@ -165,29 +153,38 @@ function generate_server(
dest: string,
dev: boolean
) {
const template_file = path.resolve(__dirname, '../templates/server.js');
const template = fs.readFileSync(template_file, 'utf-8');
const imports = [].concat(
manifest_data.server_routes.map(route =>
`import * as __${route.name} from ${stringify(posixify(`${path_to_routes}/${route.file}`))};`),
manifest_data.components.map(component =>
`import __${component.name} from ${stringify(get_file(path_to_routes, component))};`),
`import root from ${stringify(get_file(path_to_routes, manifest_data.root))};`,
manifest_data.server_routes.map((route, i) =>
`import * as route_${i} from ${stringify(posixify(`${path_to_routes}/${route.file}`))};`),
manifest_data.components.map((component, i) =>
`import component_${i}${component.has_preload ? `, { preload as preload_${i} }` : ''} from ${stringify(get_file(path_to_routes, component))};`),
`import root${manifest_data.root.has_preload ? `, { preload as root_preload }` : ''} from ${stringify(get_file(path_to_routes, manifest_data.root))};`,
`import error from ${stringify(posixify(`${path_to_routes}/_error.html`))};`
);
const component_lookup: Record<string, number> = {};
manifest_data.components.forEach((component, i) => {
component_lookup[component.name] = i;
});
let code = `
`.replace(/^\t\t/gm, '').trim();
const build_dir = posixify(path.relative(cwd, dest));
const src_dir = posixify(path.relative(cwd, src));
return `
// This file is generated by Sapper — do not edit it!
${imports.join('\n')}
const d = decodeURIComponent;
export const manifest = {
server_routes: [
${manifest_data.server_routes.map(route => `{
${manifest_data.server_routes.map((route, i) => `{
// ${route.file}
pattern: ${route.pattern},
handlers: __${route.name},
handlers: route_${i},
params: ${route.params.length > 0
? `match => ({ ${route.params.map((param, i) => `${param}: d(match[${i + 1}])`).join(', ')} })`
: `() => ({})`}
@@ -205,8 +202,9 @@ function generate_server(
const props = [
`name: "${part.component.name}"`,
`file: ${stringify(part.component.file)}`,
`component: __${part.component.name}`
];
`component: component_${component_lookup[part.component.name]}`,
part.component.has_preload && `preload: preload_${component_lookup[part.component.name]}`
].filter(Boolean);
if (part.params.length > 0) {
const params = part.params.map((param, i) => `${param}: d(match[${i + 1}])`);
@@ -220,12 +218,16 @@ function generate_server(
],
root,
root_preload${manifest_data.root.has_preload ? '' : `: () => {}`},
error
};`.replace(/^\t\t/gm, '').trim();
};
const build_dir = posixify(path.relative(cwd, dest));
const src_dir = posixify(path.relative(cwd, src));
export const build_dir = ${JSON.stringify(build_dir)};
export const src_dir = ${JSON.stringify(src_dir)};
export const dev = ${dev ? 'true' : 'false'};
`.replace(/^\t{2}/gm, '').trim();
return `// This file is generated by Sapper — do not edit it!\n` + template
.replace('__BUILD__DIR__', JSON.stringify(build_dir))
@@ -235,9 +237,6 @@ function generate_server(
}
function get_file(path_to_routes: string, component: PageComponent) {
if (component.default) {
return `./_layout.html`;
}
if (component.default) return `./layout.html`;
return posixify(`${path_to_routes}/${component.file}`);
}

View File

@@ -19,14 +19,17 @@ export type Template = {
stream: (req, res, data: Record<string, string | Promise<string>>) => void;
};
export type Store = {
get: () => any;
export type WritableStore<T> = {
set: (value: T) => void;
update: (fn: (value: T) => T) => void;
subscribe: (fn: (T: any) => void) => () => void;
};
export type PageComponent = {
default?: boolean;
name: string;
file: string;
has_preload: boolean;
};
export type Page = {

1
src/types.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare module 'svelte/compiler';

View File

@@ -1,395 +0,0 @@
import RootComponent from '__ROOT__';
import ErrorComponent from '__ERROR__';
import {
Target,
ScrollPosition,
Component,
Redirect,
ComponentLoader,
ComponentConstructor,
RootProps,
Page
} from './types';
import goto from './goto';
const ignore = __IGNORE__;
export const components: ComponentLoader[] = __COMPONENTS__;
export const pages: Page[] = __PAGES__;
let ready = false;
let root_component: Component;
let segments: string[] = [];
let current_token: {};
let root_preload: Promise<any>;
let root_data: any;
const root_props: RootProps = {
path: null,
params: null,
query: null,
child: {
segment: null,
component: null,
props: {}
}
};
export let prefetching: {
href: string;
promise: Promise<{ redirect?: Redirect, data?: any, nullable_depth?: number, new_segments?: any }>;
} = null;
export function set_prefetching(href, promise) {
prefetching = { href, promise };
}
export let store;
export function set_store(fn) {
store = fn(initial_data.store);
}
export let target: Node;
export function set_target(element) {
target = element;
}
export let uid = 1;
export function set_uid(n) {
uid = n;
}
export let cid: number;
export function set_cid(n) {
cid = n;
}
export const initial_data = typeof __SAPPER__ !== 'undefined' && __SAPPER__;
const _history = typeof history !== 'undefined' ? history : {
pushState: (state: any, title: string, href: string) => {},
replaceState: (state: any, title: string, href: string) => {},
scrollRestoration: ''
};
export { _history as history };
export const scroll_history: Record<string, ScrollPosition> = {};
export function select_route(url: URL): Target {
if (url.origin !== location.origin) return null;
if (!url.pathname.startsWith(initial_data.baseUrl)) return null;
const path = url.pathname.slice(initial_data.baseUrl.length);
// avoid accidental clashes between server routes and pages
if (ignore.some(pattern => pattern.test(path))) return;
for (let i = 0; i < pages.length; i += 1) {
const page = pages[i];
const match = page.pattern.exec(path);
if (match) {
const query: Record<string, string | string[]> = Object.create(null);
if (url.search.length > 0) {
url.search.slice(1).split('&').forEach(searchParam => {
let [, key, value] = /([^=]*)(?:=(.*))?/.exec(decodeURIComponent(searchParam));
value = (value || '').replace(/\+/g, ' ');
if (typeof query[key] === 'string') query[key] = [<string>query[key]];
if (typeof query[key] === 'object') query[key].push(value);
else query[key] = value;
});
}
return { url, path, page, match, query };
}
}
}
export function scroll_state() {
return {
x: pageXOffset,
y: pageYOffset
};
}
export function navigate(target: Target, id: number, noscroll?: boolean, hash?: string): Promise<any> {
let scroll: ScrollPosition;
if (id) {
// popstate or initial navigation
cid = id;
} else {
const current_scroll = scroll_state();
// clicked on a link. preserve scroll state
scroll_history[cid] = current_scroll;
id = cid = ++uid;
scroll_history[cid] = noscroll ? current_scroll : { x: 0, y: 0 };
}
cid = id;
if (root_component) {
root_component.set({ preloading: true });
}
const loaded = prefetching && prefetching.href === target.url.href ?
prefetching.promise :
prepare_page(target);
prefetching = null;
const token = current_token = {};
return loaded.then(({ redirect, data, nullable_depth, new_segments }) => {
if (redirect) {
return goto(redirect.location, { replaceState: true });
}
if (new_segments) {
segments = new_segments;
}
render(data, nullable_depth, scroll_history[id], noscroll, hash, token);
if (document.activeElement) document.activeElement.blur();
});
}
function render(data: any, nullable_depth: number, scroll: ScrollPosition, noscroll: boolean, hash: string, token: {}) {
if (current_token !== token) return;
if (root_component) {
// first, clear out highest-level root component
let level = data.child;
for (let i = 0; i < nullable_depth; i += 1) {
if (i === nullable_depth) break;
level = level.props.child;
}
const { component } = level;
level.component = null;
root_component.set({ child: data.child });
// then render new stuff
level.component = component;
root_component.set(data);
} else {
// first load — remove SSR'd <head> contents
const start = document.querySelector('#sapper-head-start');
const end = document.querySelector('#sapper-head-end');
if (start && end) {
while (start.nextSibling !== end) detach(start.nextSibling);
detach(start);
detach(end);
}
Object.assign(data, root_data);
root_component = new RootComponent({
target,
data,
store,
hydrate: true
});
}
if (!noscroll) {
if (hash) {
// scroll is an element id (from a hash), we need to compute y.
const deep_linked = document.querySelector(hash);
if (deep_linked) {
scroll = {
x: 0,
y: deep_linked.getBoundingClientRect().top
};
}
}
scroll_history[cid] = scroll;
if (scroll) scrollTo(scroll.x, scroll.y);
}
Object.assign(root_props, data);
ready = true;
}
export function prepare_page(target: Target): Promise<{
redirect?: Redirect;
data?: any;
nullable_depth?: number;
}> {
const { page, path, query } = target;
const new_segments = path.split('/').filter(Boolean);
let changed_from = 0;
while (
segments[changed_from] &&
new_segments[changed_from] &&
segments[changed_from] === new_segments[changed_from]
) changed_from += 1;
if (changed_from === new_segments.length) {
changed_from -= 1;
}
let redirect: Redirect = null;
let error: { statusCode: number, message: Error | string } = null;
const preload_context = {
store,
fetch: (url: string, opts?: any) => fetch(url, opts),
redirect: (statusCode: number, location: string) => {
if (redirect && (redirect.statusCode !== statusCode || redirect.location !== location)) {
throw new Error(`Conflicting redirects`);
}
redirect = { statusCode, location };
},
error: (statusCode: number, message: Error | string) => {
error = { statusCode, message };
}
};
if (!root_preload) {
root_preload = RootComponent.preload
? initial_data.preloaded[0] || RootComponent.preload.call(preload_context, {
path,
query,
params: {}
})
: {};
}
return Promise.all(page.parts.map((part, i) => {
if (i < changed_from) return null;
if (!part) return null;
return load_component(components[part.i]).then(Component => {
const req = {
path,
query,
params: part.params ? part.params(target.match) : {}
};
let preloaded;
if (ready || !initial_data.preloaded[i + 1]) {
preloaded = Component.preload
? Component.preload.call(preload_context, req)
: {};
} else {
preloaded = initial_data.preloaded[i + 1];
}
return Promise.resolve(preloaded).then(preloaded => {
return { Component, preloaded };
});
});
})).catch(err => {
error = { statusCode: 500, message: err };
return [];
}).then(results => {
if (root_data) {
return results;
} else {
return Promise.resolve(root_preload).then(value => {
root_data = value;
return results;
});
}
}).then(results => {
if (redirect) {
return { redirect, new_segments };
}
const get_params = page.parts[page.parts.length - 1].params || (() => ({}));
const params = get_params(target.match);
if (error) {
const props = {
path,
query,
params,
error: typeof error.message === 'string' ? new Error(error.message) : error.message,
status: error.statusCode
};
return {
new_segments,
data: Object.assign({}, props, {
preloading: false,
child: {
component: ErrorComponent,
props
}
})
};
}
const props = { path, query, error: null, status: null };
const data = {
path,
preloading: false,
child: Object.assign({}, root_props.child, {
segment: new_segments[0]
})
};
if (changed(query, root_props.query)) data.query = query;
if (changed(params, root_props.params)) data.params = params;
let level = data.child;
let nullable_depth = 0;
for (let i = 0; i < page.parts.length; i += 1) {
const part = page.parts[i];
if (!part) continue;
const get_params = part.params || (() => ({}));
if (i < changed_from) {
level.props.path = path;
level.props.query = query;
level.props.child = Object.assign({}, level.props.child);
nullable_depth += 1;
} else {
level.component = results[i].Component;
level.props = Object.assign({}, level.props, props, {
params: get_params(target.match),
}, results[i].preloaded);
level.props.child = {};
}
level = level.props.child;
level.segment = new_segments[i + 1];
}
return { data, nullable_depth, new_segments };
});
}
function load_css(chunk: string) {
const href = `client/${chunk}`;
if (document.querySelector(`link[href="${href}"]`)) return;
return new Promise((fulfil, reject) => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = href;
link.onload = () => fulfil();
link.onerror = reject;
document.head.appendChild(link);
});
}
export function load_component(component: ComponentLoader): Promise<ComponentConstructor> {
// TODO this is temporary — once placeholders are
// always rewritten, scratch the ternary
const promises: Array<Promise<any>> = (typeof component.css === 'string' ? [] : component.css.map(load_css));
promises.unshift(component.js());
return Promise.all(promises).then(values => values[0].default);
}
function detach(node: Node) {
node.parentNode.removeChild(node);
}
function changed(a: Record<string, string | true>, b: Record<string, string | true>) {
return JSON.stringify(a) !== JSON.stringify(b);
}

View File

@@ -1,4 +0,0 @@
export { default as start } from './start/index';
export { default as goto } from './goto/index';
export { default as prefetch } from './prefetch/index';
export { default as prefetchRoutes } from './prefetchRoutes/index';

View File

@@ -1,14 +0,0 @@
import { select_route, prefetching, set_prefetching, prepare_page } from '../app';
import { Target } from '../types';
export default function prefetch(href: string) {
const target: Target = select_route(new URL(href, document.baseURI));
if (target) {
if (!prefetching || href !== prefetching.href) {
set_prefetching(href, prepare_page(target));
}
return prefetching.promise;
}
}

View File

@@ -1,11 +0,0 @@
import { Manifest } from './types';
export const manifest: Manifest = __MANIFEST__;
export const build_dir = __BUILD__DIR__;
export const src_dir = __SRC__DIR__;
export const dev = __DEV__;
export const IGNORE = '__SAPPER__IGNORE__';

View File

@@ -1,5 +1,5 @@
import * as path from 'path';
import * as puppeteer from 'puppeteer';
import puppeteer from 'puppeteer';
import * as ports from 'port-authority';
import { fork, ChildProcess } from 'child_process';
@@ -64,11 +64,11 @@ export class AppRunner {
base: `http://localhost:${this.port}`,
// helpers
start: () => this.page.evaluate(() => start()),
prefetchRoutes: () => this.page.evaluate(() => prefetchRoutes()),
prefetch: (href: string) => this.page.evaluate((href: string) => prefetch(href), href),
goto: (href: string) => this.page.evaluate((href: string) => goto(href), href),
title: () => this.page.$eval('h1', node => node.textContent)
start: () => this.page.evaluate(() => start()).then(() => void 0),
prefetchRoutes: () => this.page.evaluate(() => prefetchRoutes()).then(() => void 0),
prefetch: (href: string) => this.page.evaluate((href: string) => prefetch(href), href).then(() => void 0),
goto: (href: string) => this.page.evaluate((href: string) => goto(href), href).then(() => void 0),
title: () => this.page.$eval('h1', node => node.textContent).then(serializable => String(serializable))
};
}

View File

@@ -22,10 +22,7 @@ export default {
emitCss: true
}),
resolve()
],
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
]
},
server: {
@@ -44,10 +41,7 @@ export default {
preferBuiltins: true
})
],
external: ['sirv', 'polka'],
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
external: ['sirv', 'polka']
},
serviceworker: {

View File

@@ -1,4 +1,4 @@
import * as sapper from '../__sapper__/client.js';
import * as sapper from '@sapper/app';
window.start = () => sapper.start({
target: document.querySelector('#sapper')

View File

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

View File

@@ -1,11 +1,13 @@
<h1>{letter}</h1>
<script context="module">
export function preload() {
return this.fetch('b.json').then(r => r.json()).then(letter => {
return { letter };
});
}
</script>
<script>
export default {
preload() {
return this.fetch('b.json').then(r => r.json()).then(letter => {
return { letter };
});
}
};
</script>
export let letter;
</script>
<h1>{letter}</h1>

View File

@@ -1,19 +1,17 @@
<button class='del' on:click='del()'>delete</button>
<script>
export default {
oncreate() {
window.deleted = null;
},
import { onMount } from 'svelte';
methods: {
del() {
fetch(`delete-test/42.json`, { method: 'DELETE' })
.then(r => r.json())
.then(data => {
window.deleted = data;
});
}
}
};
</script>
onMount(() => {
window.deleted = null;
});
function del() {
fetch(`delete-test/42.json`, { method: 'DELETE' })
.then(r => r.json())
.then(data => {
window.deleted = data;
});
}
</script>
<button class="del" on:click={del}>delete</button>

View File

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

View File

@@ -1,9 +1,7 @@
$&
<script context="module">
export function preload() {
return '$&';
}
</script>
<script>
export default {
preload() {
return '$&';
}
};
</script>
$&

View File

@@ -1,5 +1,5 @@
import polka from 'polka';
import * as sapper from '../__sapper__/server.js';
import * as sapper from '@sapper/server';
const { PORT } = process.env;

View File

@@ -1,10 +1,10 @@
import { timestamp, files, shell, routes } from '../__sapper__/service-worker.js';
import * as sapper from '@sapper/service-worker';
const ASSETS = `cache${timestamp}`;
const ASSETS = `cache${sapper.timestamp}`;
// `shell` is an array of all the files generated by webpack,
// `files` is an array of everything in the `static` directory
const to_cache = shell.concat(ASSETS);
// `app.shell` is an array of all the files generated by webpack,
// `app.files` is an array of everything in the `static` directory
const to_cache = sapper.shell.concat(sapper.files);
const cached = new Set(to_cache);
self.addEventListener('install', event => {
@@ -52,7 +52,6 @@ self.addEventListener('fetch', event => {
// which Sapper has generated for you. It's not right for every
// app, but if it's right for yours then uncomment this section
/*
if (url.origin === self.origin && routes.find(route => route.pattern.test(url.pathname))) {
event.respondWith(caches.match('/index.html'));
return;
}
@@ -65,7 +64,7 @@ self.addEventListener('fetch', event => {
// might prefer a cache-first approach to a network-first one.)
event.respondWith(
caches
.open(`offline${timestamp}`)
.open(`offline${sapper.timestamp}`)
.then(async cache => {
try {
const response = await fetch(event.request);

View File

@@ -20,19 +20,18 @@ describe('basics', function() {
let prefetchRoutes: () => Promise<void>;
let prefetch: (href: string) => Promise<void>;
let goto: (href: string) => Promise<void>;
let title: () => Promise<string>;
// hooks
before(async () => {
await build({ cwd: __dirname });
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes, prefetch, goto } = await runner.start());
({ base, page, start, prefetchRoutes, prefetch, goto, title } = await runner.start());
});
after(() => runner.end());
const title = () => page.$eval('h1', node => node.textContent);
it('serves /', async () => {
await page.goto(base);
@@ -262,7 +261,7 @@ describe('basics', function() {
await page.goto(`${base}/unsafe-replacement`);
await start();
const html = await page.evaluate(() => document.body.innerHTML);
const html = String(await page.evaluate(() => document.body.innerHTML));
assert.equal(html.indexOf('%sapper'), -1);
});
});

View File

@@ -22,10 +22,7 @@ export default {
emitCss: true
}),
resolve()
],
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
]
},
server: {
@@ -44,10 +41,7 @@ export default {
preferBuiltins: true
})
],
external: ['sirv', 'polka', 'cookie'],
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
external: ['sirv', 'polka', 'cookie']
},
serviceworker: {

View File

@@ -1,4 +1,4 @@
import * as sapper from '../__sapper__/client.js';
import * as sapper from '@sapper/app';
window.start = () => sapper.start({
target: document.querySelector('#sapper')

View File

@@ -1,11 +1,13 @@
<h1>{message}</h1>
<script context="module">
export function preload({ query }) {
return this.fetch(`credentials/test.json`, {
credentials: query.creds
}).then(r => r.json());
}
</script>
<script>
export default {
preload({ query }) {
return this.fetch(`credentials/test.json`, {
credentials: query.creds
}).then(r => r.json());
}
};
</script>
export let message;
</script>
<h1>{message}</h1>

View File

@@ -1,5 +1,5 @@
import polka from 'polka';
import * as sapper from '../__sapper__/server.js';
import * as sapper from '@sapper/server';
const { PORT } = process.env;

View File

@@ -1,10 +1,10 @@
import { timestamp, files, shell, routes } from '../__sapper__/service-worker.js';
import * as sapper from '@sapper/service-worker';
const ASSETS = `cache${timestamp}`;
const ASSETS = `cache${sapper.timestamp}`;
// `shell` is an array of all the files generated by webpack,
// `files` is an array of everything in the `static` directory
const to_cache = shell.concat(ASSETS);
const to_cache = sapper.shell.concat(sapper.files);
const cached = new Set(to_cache);
self.addEventListener('install', event => {
@@ -65,7 +65,7 @@ self.addEventListener('fetch', event => {
// might prefer a cache-first approach to a network-first one.)
event.respondWith(
caches
.open(`offline${timestamp}`)
.open(`offline${sapper.timestamp}`)
.then(async cache => {
try {
const response = await fetch(event.request);

View File

@@ -22,10 +22,7 @@ export default {
emitCss: true
}),
resolve()
],
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
]
},
server: {
@@ -44,10 +41,7 @@ export default {
preferBuiltins: true
})
],
external: ['sirv', 'polka'],
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
external: ['sirv', 'polka']
},
serviceworker: {

View File

@@ -1,4 +1,4 @@
import * as sapper from '../__sapper__/client.js';
import * as sapper from '@sapper/app';
window.start = () => sapper.start({
target: document.querySelector('#sapper')

View File

@@ -1,11 +1,13 @@
<svelte:component this={Title}/>
<script>
export default {
oncreate() {
import('./_components/Title.html').then(({ default: Title }) => {
this.set({ Title });
});
}
};
</script>
import { onMount } from 'svelte';
export let Title;
onMount(() => {
import('./_components/Title.html').then(mod => {
Title = mod.default;
});
});
</script>
<svelte:component this={Title}/>

View File

@@ -1,5 +1,5 @@
import polka from 'polka';
import * as sapper from '../__sapper__/server.js';
import * as sapper from '@sapper/server';
const { PORT } = process.env;

View File

@@ -1,10 +1,10 @@
import { timestamp, files, shell, routes } from '../__sapper__/service-worker.js';
import * as sapper from '@sapper/service-worker';
const ASSETS = `cache${timestamp}`;
const ASSETS = `cache${sapper.timestamp}`;
// `shell` is an array of all the files generated by webpack,
// `files` is an array of everything in the `static` directory
const to_cache = shell.concat(ASSETS);
const to_cache = sapper.shell.concat(sapper.files);
const cached = new Set(to_cache);
self.addEventListener('install', event => {
@@ -65,7 +65,7 @@ self.addEventListener('fetch', event => {
// might prefer a cache-first approach to a network-first one.)
event.respondWith(
caches
.open(`offline${timestamp}`)
.open(`offline${sapper.timestamp}`)
.then(async cache => {
try {
const response = await fetch(event.request);

View File

@@ -22,10 +22,7 @@ export default {
emitCss: true
}),
resolve()
],
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
]
},
server: {
@@ -44,10 +41,7 @@ export default {
preferBuiltins: true
})
],
external: ['sirv', 'polka'],
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
external: ['sirv', 'polka']
},
serviceworker: {

View File

@@ -1,4 +1,4 @@
import * as sapper from '../__sapper__/client.js';
import * as sapper from '@sapper/app';
window.start = () => sapper.start({
target: document.querySelector('#sapper')

View File

@@ -1,11 +1,14 @@
<h1>{slug} {JSON.stringify(query)}</h1>
<script context="module">
export function preload({ params }) {
return {
slug: params.slug
};
}
</script>
<script>
export default {
preload({ params }) {
return {
slug: params.slug
};
}
};
</script>
import { page } from '@sapper/app';
export let slug;
</script>
<h1>{slug} {JSON.stringify($page.query)}</h1>

View File

@@ -1,11 +1,13 @@
<h1>{phrase}</h1>
<script context="module">
export function preload() {
return this.fetch('fünke.json').then(r => r.json()).then(phrase => {
return { phrase };
});
}
</script>
<script>
export default {
preload() {
return this.fetch('fünke.json').then(r => r.json()).then(phrase => {
return { phrase };
});
}
};
</script>
export let phrase;
</script>
<h1>{phrase}</h1>

View File

@@ -1,5 +1,5 @@
import polka from 'polka';
import * as sapper from '../__sapper__/server.js';
import * as sapper from '@sapper/server';
const { PORT } = process.env;

View File

@@ -1,10 +1,10 @@
import { timestamp, files, shell, routes } from '../__sapper__/service-worker.js';
import * as sapper from '@sapper/service-worker';
const ASSETS = `cache${timestamp}`;
const ASSETS = `cache${sapper.timestamp}`;
// `shell` is an array of all the files generated by webpack,
// `files` is an array of everything in the `static` directory
const to_cache = shell.concat(ASSETS);
const to_cache = sapper.shell.concat(sapper.files);
const cached = new Set(to_cache);
self.addEventListener('install', event => {
@@ -65,7 +65,7 @@ self.addEventListener('fetch', event => {
// might prefer a cache-first approach to a network-first one.)
event.respondWith(
caches
.open(`offline${timestamp}`)
.open(`offline${sapper.timestamp}`)
.then(async cache => {
try {
const response = await fetch(event.request);

View File

@@ -22,10 +22,7 @@ export default {
emitCss: true
}),
resolve()
],
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
]
},
server: {
@@ -44,10 +41,7 @@ export default {
preferBuiltins: true
})
],
external: ['sirv', 'polka'],
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
external: ['sirv', 'polka']
},
serviceworker: {

View File

@@ -1,4 +1,4 @@
import * as sapper from '../__sapper__/client.js';
import * as sapper from '@sapper/app';
window.start = () => sapper.start({
target: document.querySelector('#sapper')

View File

@@ -1,19 +1,21 @@
<h1>{post.title}</h1>
<script context="module">
export function preload({ params }) {
const { slug } = params;
return this.fetch(`blog/${slug}.json`).then(r => {
return r.json().then(data => {
if (r.status !== 200) {
this.error(r.status, data);
}
return data;
});
});
}
</script>
<script>
export default {
preload({ params }) {
const { slug } = params;
export let post;
</script>
return this.fetch(`blog/${slug}.json`).then(r => {
return r.json().then(data => {
if (r.status !== 200) {
this.error(r.status, data);
}
return data;
});
});
}
};
</script>
<h1>{post.title}</h1>

View File

@@ -1,7 +1,5 @@
<script>
export default {
preload() {
this.error(420, 'Enhance your calm');
}
};
<script context="module">
export function preload() {
this.error(420, 'Enhance your calm');
}
</script>

View File

@@ -1,7 +1,5 @@
<script>
export default {
preload() {
throw new Error('nope');
}
};
<script context="module">
export function preload() {
throw new Error('nope');
}
</script>

View File

@@ -1,5 +1,5 @@
import polka from 'polka';
import * as sapper from '../__sapper__/server.js';
import * as sapper from '@sapper/server';
const { PORT } = process.env;

View File

@@ -1,10 +1,10 @@
import { timestamp, files, shell, routes } from '../__sapper__/service-worker.js';
import * as sapper from '@sapper/service-worker';
const ASSETS = `cache${timestamp}`;
const ASSETS = `cache${sapper.timestamp}`;
// `shell` is an array of all the files generated by webpack,
// `files` is an array of everything in the `static` directory
const to_cache = shell.concat(ASSETS);
const to_cache = sapper.shell.concat(sapper.files);
const cached = new Set(to_cache);
self.addEventListener('install', event => {
@@ -65,7 +65,7 @@ self.addEventListener('fetch', event => {
// might prefer a cache-first approach to a network-first one.)
event.respondWith(
caches
.open(`offline${timestamp}`)
.open(`offline${sapper.timestamp}`)
.then(async cache => {
try {
const response = await fetch(event.request);

View File

@@ -22,10 +22,7 @@ export default {
emitCss: true
}),
resolve()
],
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
]
},
server: {
@@ -44,10 +41,7 @@ export default {
preferBuiltins: true
})
],
external: ['sirv', 'polka'],
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
external: ['sirv', 'polka']
},
serviceworker: {

View File

@@ -1,4 +1,4 @@
import * as sapper from '../__sapper__/client.js';
import * as sapper from '@sapper/app';
window.start = () => sapper.start({
target: document.querySelector('#sapper')

View File

@@ -1,11 +1,13 @@
<h1>{post.title}</h1>
<script context="module">
export function preload({ params }) {
return this.fetch(`blog/${params.slug}.json`).then(r => r.json()).then(post => {
return { post };
});
}
</script>
<script>
export default {
preload({ params }) {
return this.fetch(`blog/${params.slug}.json`).then(r => r.json()).then(post => {
return { post };
});
}
};
</script>
export let post;
</script>
<h1>{post.title}</h1>

View File

@@ -1,15 +1,17 @@
<script context="module">
export function preload() {
return this.fetch('blog.json').then(r => r.json()).then(posts => {
return { posts };
});
}
</script>
<script>
export let posts;
</script>
<h1>blog</h1>
{#each posts as post}
<p><a href="blog/{post.slug}">{post.title}</a></p>
{/each}
<script>
export default {
preload() {
return this.fetch('blog.json').then(r => r.json()).then(posts => {
return { posts };
});
}
};
</script>
{/each}

View File

@@ -1,6 +1,6 @@
import sirv from 'sirv';
import polka from 'polka';
import * as sapper from '../__sapper__/server.js';
import * as sapper from '@sapper/server';
const { PORT, NODE_ENV } = process.env;
const dev = NODE_ENV === 'development';

View File

@@ -1,10 +1,10 @@
import { timestamp, files, shell, routes } from '../__sapper__/service-worker.js';
import * as sapper from '@sapper/service-worker';
const ASSETS = `cache${timestamp}`;
const ASSETS = `cache${sapper.timestamp}`;
// `shell` is an array of all the files generated by webpack,
// `files` is an array of everything in the `static` directory
const to_cache = shell.concat(ASSETS);
const to_cache = sapper.shell.concat(sapper.files);
const cached = new Set(to_cache);
self.addEventListener('install', event => {
@@ -65,7 +65,7 @@ self.addEventListener('fetch', event => {
// might prefer a cache-first approach to a network-first one.)
event.respondWith(
caches
.open(`offline${timestamp}`)
.open(`offline${sapper.timestamp}`)
.then(async cache => {
try {
const response = await fetch(event.request);

View File

@@ -22,10 +22,7 @@ export default {
emitCss: true
}),
resolve()
],
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
]
},
server: {
@@ -44,10 +41,7 @@ export default {
preferBuiltins: true
})
],
external: ['sirv', 'polka'],
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
external: ['sirv', 'polka']
},
serviceworker: {

View File

@@ -1,4 +1,4 @@
import * as sapper from '../__sapper__/client.js';
import * as sapper from '@sapper/app';
window.start = () => sapper.start({
target: document.querySelector('#sapper')

View File

@@ -1,11 +1,13 @@
<h1>{letter}</h1>
<script context="module">
export function preload() {
return this.fetch('b.json').then(r => r.json()).then(letter => {
return { letter };
});
}
</script>
<script>
export default {
preload() {
return this.fetch('b.json').then(r => r.json()).then(letter => {
return { letter };
});
}
};
</script>
export let letter;
</script>
<h1>{letter}</h1>

View File

@@ -1,5 +1,5 @@
import polka from 'polka';
import * as sapper from '../__sapper__/server.js';
import * as sapper from '@sapper/server';
const { PORT } = process.env;

View File

@@ -1,10 +1,10 @@
import { timestamp, files, shell, routes } from '../__sapper__/service-worker.js';
import * as sapper from '@sapper/service-worker';
const ASSETS = `cache${timestamp}`;
const ASSETS = `cache${sapper.timestamp}`;
// `shell` is an array of all the files generated by webpack,
// `files` is an array of everything in the `static` directory
const to_cache = shell.concat(ASSETS);
const to_cache = sapper.shell.concat(sapper.files);
const cached = new Set(to_cache);
self.addEventListener('install', event => {
@@ -65,7 +65,7 @@ self.addEventListener('fetch', event => {
// might prefer a cache-first approach to a network-first one.)
event.respondWith(
caches
.open(`offline${timestamp}`)
.open(`offline${sapper.timestamp}`)
.then(async cache => {
try {
const response = await fetch(event.request);

View File

@@ -22,10 +22,7 @@ export default {
emitCss: true
}),
resolve()
],
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
]
},
server: {
@@ -44,10 +41,7 @@ export default {
preferBuiltins: true
})
],
external: ['sirv', 'polka'],
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
external: ['sirv', 'polka']
},
serviceworker: {

View File

@@ -1,4 +1,4 @@
import * as sapper from '../__sapper__/client.js';
import * as sapper from '@sapper/app';
window.start = () => sapper.start({
target: document.querySelector('#sapper')

View File

@@ -1,20 +1,18 @@
<span>z: {segment} {count}</span>
<a href="foo/bar/qux">click me</a>
<script>
<script context="module">
import counts from '../_counts.js';
export default {
preload() {
return {
count: counts.z += 1
};
},
export function preload() {
return {
count: counts.z += 1
};
}
</script>
oncreate() {
this.set({
segment: this.get().params.z
});
}
};
</script>
<script>
import { page } from '@sapper/app';
export let count;
</script>
<span>z: {$page.params.z} {count}</span>
<a href="foo/bar/qux">click me</a>

View File

@@ -1,22 +1,21 @@
<span>y: {segment} {count}</span>
<svelte:component this={child.component} {...child.props}/>
<span>child segment: {child.segment}</span>
<script>
<script context="module">
import counts from '../_counts.js';
export default {
preload() {
return {
count: counts.y += 1
};
},
export function preload() {
return {
count: counts.y += 1
};
}
</script>
oncreate() {
this.set({
segment: this.get().params.y
});
}
};
</script>
<script>
import { page } from '@sapper/app';
export let count;
export let child;
</script>
<span>y: {$page.params.y} {count}</span>
<svelte:component this={child.component} {...child.props}/>
<span>child segment: {child.segment}</span>

View File

@@ -1,5 +1,5 @@
import polka from 'polka';
import * as sapper from '../__sapper__/server.js';
import * as sapper from '@sapper/server';
const { PORT } = process.env;

View File

@@ -1,10 +1,10 @@
import { timestamp, files, shell, routes } from '../__sapper__/service-worker.js';
import * as sapper from '@sapper/service-worker';
const ASSETS = `cache${timestamp}`;
const ASSETS = `cache${sapper.timestamp}`;
// `shell` is an array of all the files generated by webpack,
// `files` is an array of everything in the `static` directory
const to_cache = shell.concat(ASSETS);
const to_cache = sapper.shell.concat(sapper.files);
const cached = new Set(to_cache);
self.addEventListener('install', event => {
@@ -65,7 +65,7 @@ self.addEventListener('fetch', event => {
// might prefer a cache-first approach to a network-first one.)
event.respondWith(
caches
.open(`offline${timestamp}`)
.open(`offline${sapper.timestamp}`)
.then(async cache => {
try {
const response = await fetch(event.request);

View File

@@ -26,10 +26,18 @@ describe('layout', function() {
it('only recreates components when necessary', async () => {
await page.goto(`${base}/foo/bar/baz`);
await start();
const text1 = await page.evaluate(() => document.querySelector('#sapper').textContent);
assert.deepEqual(text1.split('\n').filter(Boolean), [
const text1 = String(await page.evaluate(() => document.querySelector('#sapper').textContent));
assert.deepEqual(text1.split('\n').filter(Boolean).map(str => str.trim()), [
'y: bar 1',
'z: baz 1',
'click me',
'child segment: baz'
]);
await start();
const text2 = String(await page.evaluate(() => document.querySelector('#sapper').textContent));
assert.deepEqual(text2.split('\n').filter(Boolean).map(str => str.trim()), [
'y: bar 1',
'z: baz 1',
'click me',
@@ -39,8 +47,8 @@ describe('layout', function() {
await page.click('[href="foo/bar/qux"]');
await wait(50);
const text2 = await page.evaluate(() => document.querySelector('#sapper').textContent);
assert.deepEqual(text2.split('\n').filter(Boolean), [
const text3 = String(await page.evaluate(() => document.querySelector('#sapper').textContent));
assert.deepEqual(text3.split('\n').filter(Boolean).map(str => str.trim()), [
'y: bar 1',
'z: qux 2',
'click me',

View File

@@ -22,10 +22,7 @@ export default {
emitCss: true
}),
resolve()
],
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
]
},
server: {
@@ -44,10 +41,7 @@ export default {
preferBuiltins: true
})
],
external: ['sirv', 'polka'],
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
external: ['sirv', 'polka']
},
serviceworker: {

View File

@@ -1,4 +1,4 @@
import * as sapper from '../__sapper__/client.js';
import * as sapper from '@sapper/app';
window.start = () => sapper.start({
target: document.querySelector('#sapper')

View File

@@ -1,15 +1,20 @@
{#if preloading}
<script context="module">
export function preload() {
return {
rootPreloadFunctionRan: true
};
}
</script>
<script>
import { preloading } from '@sapper/app';
export let child;
export let rootPreloadFunctionRan;
</script>
{#if $preloading}
<progress class='preloading-progress' value=0.5/>
{/if}
<svelte:component this={child.component} {rootPreloadFunctionRan} {...child.props}/>
<script>
export default {
preload() {
return {
rootPreloadFunctionRan: true
};
}
};
</script>
<svelte:component this={child.component} {rootPreloadFunctionRan} {...child.props}/>

View File

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

View File

@@ -1,17 +1,19 @@
<h1>{foo.bar()}</h1>
<script context="module">
export function preload() {
class Foo {
bar() {
return 42;
}
}
return {
foo: new Foo()
};
}
</script>
<script>
export default {
preload() {
class Foo {
bar() {
return 42;
}
}
export let foo;
</script>
return {
foo: new Foo()
};
}
};
</script>
<h1>{foo.bar()}</h1>

View File

@@ -1,11 +1,13 @@
<h1>{set.has('x')}</h1>
<script context="module">
export function preload() {
return {
set: new Set(['x'])
};
}
</script>
<script>
export default {
preload() {
return {
set: new Set(['x'])
};
}
};
</script>
export let set;
</script>
<h1>{set.has('x')}</h1>

View File

@@ -1,15 +1,13 @@
<h1>This page should never render</h1>
<script context="module">
export function preload() {
return new Promise(fulfil => {
if (typeof window !== 'undefined') {
window.fulfil = fulfil;
} else {
fulfil({});
}
});
}
</script>
<script>
export default {
preload() {
return new Promise(fulfil => {
if (typeof window !== 'undefined') {
window.fulfil = fulfil;
} else {
fulfil({});
}
});
}
};
</script>
<h1>This page should never render</h1>

View File

@@ -1,5 +1,5 @@
import polka from 'polka';
import * as sapper from '../__sapper__/server.js';
import * as sapper from '@sapper/server';
const { PORT } = process.env;

View File

@@ -1,10 +1,10 @@
import { timestamp, files, shell, routes } from '../__sapper__/service-worker.js';
import * as sapper from '@sapper/service-worker';
const ASSETS = `cache${timestamp}`;
const ASSETS = `cache${sapper.timestamp}`;
// `shell` is an array of all the files generated by webpack,
// `files` is an array of everything in the `static` directory
const to_cache = shell.concat(ASSETS);
const to_cache = sapper.shell.concat(sapper.files);
const cached = new Set(to_cache);
self.addEventListener('install', event => {
@@ -65,7 +65,7 @@ self.addEventListener('fetch', event => {
// might prefer a cache-first approach to a network-first one.)
event.respondWith(
caches
.open(`offline${timestamp}`)
.open(`offline${sapper.timestamp}`)
.then(async cache => {
try {
const response = await fetch(event.request);

View File

@@ -1,4 +1,3 @@
import * as path from 'path';
import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import { build } from '../../../api';
@@ -17,19 +16,18 @@ describe('preloading', function() {
// helpers
let start: () => Promise<void>;
let prefetchRoutes: () => Promise<void>;
let title: () => Promise<string>;
// hooks
before(async () => {
await build({ cwd: __dirname });
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes } = await runner.start());
({ base, page, start, prefetchRoutes, title } = await runner.start());
});
after(() => runner.end());
const title = () => page.$eval('h1', node => node.textContent);
it('serializes Set objects returned from preload', async () => {
await page.goto(`${base}/preload-values/set`);

View File

@@ -22,10 +22,7 @@ export default {
emitCss: true
}),
resolve()
],
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
]
},
server: {
@@ -44,10 +41,7 @@ export default {
preferBuiltins: true
})
],
external: ['sirv', 'polka'],
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
external: ['sirv', 'polka']
},
serviceworker: {

Some files were not shown because too many files have changed in this diff Show More