Compare commits

...

66 Commits

Author SHA1 Message Date
Richard Harris
f0588ed9ab update to node stable 2019-04-27 13:05:51 -04:00
Richard Harris
01dd08849a Merge branch 'master' of github.com:sveltejs/sapper 2019-04-27 13:05:28 -04:00
Richard Harris
534a96214e pin node version to 11, for now 2019-04-27 13:05:22 -04:00
Rich Harris
9725767fe2 Merge pull request #593 from thgh/patch-4
Provide session in layout preload
2019-04-27 12:19:58 -04:00
Rich Harris
90ec61f14d Merge pull request #603 from sveltejs/gh-602
fix handling of empty hrefs during export
2019-04-27 12:18:45 -04:00
Rich Harris
4f9919e95c Merge pull request #610 from cudr/navigate_fix
Navigate from /a/[id] to /b/[id] fix
2019-04-27 11:56:03 -04:00
Rich Harris
b089ca42ff Merge pull request #608 from cudr/preload_return_fix
Don't crash if preload return empty
2019-04-27 11:15:14 -04:00
Rich Harris
6cb4030b2b Merge branch 'master' into navigate_fix 2019-04-27 11:11:27 -04:00
Rich Harris
a89f7b01bb Merge pull request #616 from Seb35/fix-swindex-base-path
Fix base-path with service-worker-index.html - fixes #579
2019-04-27 11:07:36 -04:00
Rich Harris
96a068245b Merge pull request #620 from cudr/error_page_hooks
Error page lifecycle
2019-04-27 11:06:28 -04:00
Richard Harris
0862d0e2c8 add a test for server-route-as-middleware 2019-04-27 10:55:22 -04:00
Richard Harris
a26f8600c1 Merge branch 'html' of https://github.com/akihikodaki/sapper into akihikodaki-html 2019-04-27 10:47:49 -04:00
Rich Harris
f9d1dc5d3f Merge pull request #622 from artemjackson/patch-2
Fixed %sapper.styles% injection for webpack apps
2019-04-21 13:42:21 -04:00
Rich Harris
52c4106d2c in this house we use tabs 2019-04-21 13:41:52 -04:00
Akihiko Odaki
0fd332135e Allow to have middleware for the path same with a HTML page
The feature was introduced with 9e2d0a7fbc,
but regressed with commit 8dc52a04e4.
2019-04-13 19:42:19 +09:00
cudr
9bb8bfa884 receive preloaded root_data 2019-04-08 20:07:42 +03:00
Artyom Stepanishchev
01c0097acb Fixed %sapper.styles% injection for webpack builds 2019-04-08 18:51:35 +03:00
cudr
dcf726a89b add missed semicolon 2019-04-06 02:51:05 +03:00
cudr
9e60a71cf5 clean up 2019-04-06 02:50:00 +03:00
cudr
3a9d457389 execute error page hooks 2019-04-06 02:32:11 +03:00
Seb35
1e9cd84854 Fix base-path with service-worker-index.html - fixes #579 2019-03-25 20:33:43 +01:00
cudr
d2cda4b6c0 add test 2019-03-15 05:13:18 +03:00
cudr
0dd2d2eb4a complete fix 2019-03-15 04:57:10 +03:00
cudr
6bf3dd04dd fix empty preload 2019-03-13 15:18:23 +03:00
Conduitry
6d5aa9a35d fix handling of empty hrefs during export (#602) 2019-03-11 10:39:35 -04:00
Richard Harris
7be7e1eb9f bump alpha version 2019-03-08 09:25:54 -05:00
Richard Harris
ca7973465b Merge branch 'master' of github.com:sveltejs/sapper 2019-03-08 08:37:08 -05:00
Rich Harris
f7c88df3be Merge pull request #596 from sveltejs/recover-alpha-10
various lost changes and fixes from 0.26.0-alpha.10
2019-03-08 08:35:47 -05:00
Conduitry
74c66b784f Server-side preload check fixes (fixes #575, #594, #598) 2019-03-08 08:34:36 -05:00
Conduitry
9e9bd10333 various lost changes and fixes from 0.26.0-alpha.10 2019-03-07 13:18:40 -05:00
Thomas Ghysels
8858301fed Provide session in layout preload 2019-03-05 15:22:40 +01:00
Rich Harris
9540383796 bump alpha version 2019-03-04 19:44:22 -05:00
halfnelson
b5edf0edd5 Fix export race condition (#585)
* Await all items in the export queue before killing server
2019-03-04 05:21:30 -05:00
Richard Harris
6dad750942 Merge branch 'better-errors' 2019-02-24 20:56:20 -05:00
Richard Harris
eee9d21900 tidy up 2019-02-24 20:56:16 -05:00
Richard Harris
55505571f8 extract entry css correctly 2019-02-24 20:44:19 -05:00
Richard Harris
4fe3c96c2d print file/location/frame when encountering an initial error in dev 2019-02-22 09:28:08 -05:00
Rich Harris
411e2594af bump alpha version 2019-02-21 17:05:06 -05:00
Rich Harris
e0de230e13 Slot-based routing (#573) 2019-02-21 16:34:07 -05:00
Tamas Ridly
c637687922 Fixed README - Get started section 2019-02-18 08:01:09 -05:00
Richard Harris
57fe5bdfa2 bump alpha version 2019-02-17 10:10:02 -05:00
Rich Harris
b2b476abb1 Merge pull request #568 from nolanlawson/nolan/preload-in-export
fix: add link rel=preload for exported sites
2019-02-17 09:55:08 -04:00
Rich Harris
ad0ebb8a69 Merge branch 'master' into nolan/preload-in-export 2019-02-17 09:51:03 -04:00
Rich Harris
130eafbd0a Merge pull request #566 from nolanlawson/nolan/issue-565
fix: fix "sapper export" with a custom PORT
2019-02-17 09:48:02 -04:00
Rich Harris
9d2ce6d852 Merge pull request #563 from nolanlawson/patch-2
Remove unnecessary index.html file serve
2019-02-17 09:46:55 -04:00
Rich Harris
a476d21c9b Merge pull request #561 from thgh/patch-3
Use getElementById to avoid querySelector error
2019-02-17 09:45:57 -04:00
Rich Harris
30b4b6660b Merge pull request #569 from sveltejs/update-rollup
update Rollup, remove some superfluous deps
2019-02-17 09:45:02 -04:00
Richard Harris
cfd10c6f61 remove some more unused deps 2019-02-17 08:32:02 -05:00
Richard Harris
82a4973943 remove node 6 from ci matrix 2019-02-17 08:31:48 -05:00
Richard Harris
0609a92f3a update Rollup, remove some superfluous deps 2019-02-17 08:23:30 -05:00
Nolan Lawson
37780656fd fix incorrect test 2019-02-16 00:13:27 -08:00
Nolan Lawson
351ab13d29 fix: add link rel=preload for exported sites 2019-02-16 00:07:06 -08:00
Nolan Lawson
795da23418 fix: fix "sapper export" with a custom PORT
Fixes #565
2019-02-15 18:50:34 -08:00
Nolan Lawson
1f1211b7b4 Remove unnecessary index.html file serve
As far as I can tell, `/index.html` should no longer exist (outside of `sapper export`, which doesn't apply here). After #525 was merged, we're now using `/service-worker-index.html`, but that's handled by `get_page_handler.ts`, not here. So this code is doing nothing.
2019-02-14 19:14:14 -08:00
Thomas Ghysels
acafeac1cc Fix #561 2019-02-13 17:59:32 +01:00
Rich Harris
82e637ea7c Merge pull request #559 from sveltejs/svelte-extension
Use .svelte extension
2019-02-08 11:47:37 -05:00
Rich Harris
14ace57612 rename .html files to .svelte 2019-02-08 11:40:30 -05:00
Rich Harris
84a0ae562f support .svelte or .html extensions 2019-02-08 11:34:33 -05:00
Rich Harris
8870b58766 bump alpha version 2019-02-08 10:42:46 -05:00
Rich Harris
54506c1eb6 bump alpha version 2019-02-07 08:47:32 -05:00
Rich Harris
4f6b2dcb7c fix kleur thing 2019-02-07 08:47:02 -05:00
Richard Harris
0a87204593 Merge branch 'svelte-3' 2019-02-04 07:43:11 -05:00
Richard Harris
720cf8a859 treat foo.html and foo/index.html the same when generating manifest 2019-02-04 07:42:45 -05:00
Rich Harris
ca034d0857 Support Svelte 3
fixes #546, #551, #552, #554
2019-02-03 14:29:47 -05:00
Richard Harris
96b9d19715 remove typescript in favour of sucrase 2019-02-03 14:28:59 -05:00
Richard Harris
293da8bcd1 fix a cmd-f/cmd-r mistake 2019-02-03 14:12:17 -05:00
117 changed files with 1511 additions and 2139 deletions

View File

@@ -3,7 +3,6 @@ sudo: false
language: node_js
node_js:
- "6"
- "stable"
env:

View File

@@ -11,9 +11,11 @@ Sapper is a framework for building high-performance universal web apps. [Read th
## Get started
Clone the [starter project template](https://github.com/sveltejs/sapper-template) with [degit](https://github.com/rich-harris/degit)...
When cloning you have to choose between rollup or webpack:
```bash
npx degit sveltejs/sapper-template my-app
npx degit "sveltejs/sapper-template#rollup" my-app
# or: npx degit "sveltejs/sapper-template#webpack" my-app
```
...then install dependencies and start the dev server...

View File

@@ -10,7 +10,7 @@ build: off
environment:
matrix:
# node.js
- nodejs_version: 10.5
- nodejs_version: 11
install:
- ps: Install-Product node $env:nodejs_version

2504
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "sapper",
"version": "0.26.0-alpha.3",
"version": "0.26.0-alpha.12",
"description": "Military-grade apps, engineered by Svelte",
"bin": {
"sapper": "./sapper"
@@ -19,18 +19,15 @@
},
"dependencies": {
"html-minifier": "^3.5.21",
"http-link-header": "^1.0.2",
"shimport": "0.0.14",
"source-map-support": "^0.5.10",
"sourcemap-codec": "^1.4.4",
"string-hash": "^1.1.3",
"tslib": "^1.9.3"
"string-hash": "^1.1.3"
},
"devDependencies": {
"@types/mkdirp": "^0.5.2",
"@types/mocha": "^5.2.5",
"@types/node": "^10.12.21",
"@types/puppeteer": "^1.11.3",
"@types/rimraf": "^2.0.2",
"agadoo": "^1.0.1",
"cheap-watch": "^1.0.2",
"cookie": "^0.3.1",
@@ -38,7 +35,6 @@
"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.3.0",
"npm-run-all": "^4.1.5",
@@ -47,7 +43,6 @@
"pretty-bytes": "^5.1.0",
"puppeteer": "^1.12.0",
"require-relative": "^0.8.7",
"rimraf": "^2.6.3",
"rollup": "^1.1.2",
"rollup-plugin-commonjs": "^9.2.0",
"rollup-plugin-json": "^3.1.0",
@@ -55,16 +50,12 @@
"rollup-plugin-replace": "^2.1.0",
"rollup-plugin-string": "^2.0.2",
"rollup-plugin-sucrase": "^2.1.0",
"rollup-plugin-svelte": "^5.0.1",
"rollup-plugin-typescript": "^1.0.0",
"rollup-plugin-svelte": "^5.0.3",
"sade": "^1.4.2",
"sander": "^0.6.0",
"sirv": "^0.2.2",
"sucrase": "^3.9.5",
"svelte": "^3.0.0-alpha27",
"svelte-loader": "^2.12.0",
"ts-node": "^8.0.2",
"typescript": "^3.3.1",
"svelte": "^3.0.0-beta.11",
"svelte-loader": "^2.13.3",
"webpack": "^4.29.0",
"webpack-format-messages": "^2.0.5",
"yootils": "0.0.14"

View File

@@ -9,7 +9,8 @@ import { builtinModules } from 'module';
const external = [].concat(
Object.keys(pkg.dependencies),
Object.keys(process.binding('natives')),
'sapper/core.js'
'sapper/core.js',
'svelte/compiler'
);
function template(kind, external) {
@@ -38,7 +39,7 @@ function template(kind, external) {
export default [
template('app', id => /^(svelte\/?|@sapper\/)/.test(id)),
template('server', id => builtinModules.includes(id)),
template('server', id => /^(svelte\/?|@sapper\/)/.test(id) || builtinModules.includes(id)),
{
input: [
@@ -65,4 +66,4 @@ export default [
})
]
}
];
];

View File

@@ -0,0 +1,7 @@
<h1>{status}</h1>
<p>{error.message}</p>
{#if process.env.NODE_ENV === 'development'}
<pre>{error.stack}</pre>
{/if}

View File

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

View File

@@ -0,0 +1 @@
<slot></slot>

View File

@@ -1,5 +1,5 @@
import { writable } from 'svelte/store.mjs';
import Sapper from '@sapper/internal/Sapper.html';
import App from '@sapper/internal/App.svelte';
import { stores } from '@sapper/internal/shared';
import { Root, root_preload, ErrorComponent, ignore, components, routes } from '@sapper/internal/manifest-client';
import {
@@ -10,6 +10,7 @@ import {
ComponentLoader,
ComponentConstructor,
Route,
Query,
Page
} from './types';
import goto from './goto';
@@ -80,6 +81,20 @@ export { _history as history };
export const scroll_history: Record<string, ScrollPosition> = {};
export function extract_query(search: string) {
const query = Object.create(null);
if (search.length > 0) {
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;
});
}
return query;
}
export function select_target(url: URL): Target {
if (url.origin !== location.origin) return null;
if (!url.pathname.startsWith(initial_data.baseUrl)) return null;
@@ -93,18 +108,9 @@ export function select_target(url: URL): Target {
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;
});
}
if (match) {
const query: Query = extract_query(url.search);
const part = route.parts[route.parts.length - 1];
const params = part.params ? part.params(match) : {};
@@ -115,6 +121,35 @@ export function select_target(url: URL): Target {
}
}
export function handle_error(url: URL) {
const { pathname, search } = location;
const { session, preloaded, status, error } = initial_data;
if (!root_preloaded) {
root_preloaded = preloaded && preloaded[0]
}
const props = {
error,
status,
session,
level0: {
props: root_preloaded
},
level1: {
props: {
status,
error
},
component: ErrorComponent
},
segments: preloaded
}
const query = extract_query(search);
render(null, [], props, { path: pathname, query, params: {} });
}
export function scroll_state() {
return {
x: pageXOffset,
@@ -158,7 +193,7 @@ export async function navigate(target: Target, id: number, noscroll?: boolean, h
if (hash) {
// scroll is an element id (from a hash), we need to compute y.
const deep_linked = document.querySelector(hash);
const deep_linked = document.getElementById(hash.slice(1));
if (deep_linked) {
scroll = {
@@ -180,8 +215,13 @@ async function render(redirect: Redirect, branch: any[], props: any, page: Page)
stores.preloading.set(false);
if (root_component) {
root_component.props = props;
root_component.$set(props);
} else {
props.session = session;
props.level0 = {
props: await root_preloaded
};
// first load — remove SSR'd <head> contents
const start = document.querySelector('#sapper-head-start');
const end = document.querySelector('#sapper-head-end');
@@ -192,13 +232,9 @@ async function render(redirect: Redirect, branch: any[], props: any, page: Page)
detach(end);
}
root_component = new Sapper({
root_component = new App({
target,
props: {
Root,
props,
session
},
props,
hydrate: true
});
}
@@ -211,13 +247,14 @@ async function render(redirect: Redirect, branch: any[], props: any, page: Page)
export async function hydrate_target(target: Target): Promise<{
redirect?: Redirect;
props?: any;
branch?: Array<{ Component: ComponentConstructor, preload: (page) => Promise<any>, segment: string }>
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 props = { error: null, status: 200, segments: [segments[0]] };
const preload_context = {
fetch: (url: string, opts?: any) => fetch(url, opts),
@@ -227,8 +264,9 @@ export async function hydrate_target(target: Target): Promise<{
}
redirect = { statusCode, location };
},
error: (statusCode: number, message: Error | string) => {
error = { statusCode, message };
error: (status: number, error: Error | string) => {
props.error = typeof error === 'string' ? new Error(error) : error;
props.status = status;
}
};
@@ -241,15 +279,19 @@ export async function hydrate_target(target: Target): Promise<{
}
let branch;
let l = 1;
try {
branch = await Promise.all(route.parts.map(async (part, i) => {
props.segments[l] = segments[i + 1]; // TODO make this less confusing
if (!part) return null;
const segment = segments[i];
if (!session_dirty && current_branch[i] && current_branch[i].segment === segment) return current_branch[i];
const j = l++;
const { default: Component, preload } = await load_component(components[part.i]);
const segment = segments[i];
if (!session_dirty && current_branch[i] && current_branch[i].segment === segment && current_branch[i].part === part.i) return current_branch[i];
const { default: component, preload } = await load_component(components[part.i]);
let preloaded;
if (ready || !initial_data.preloaded[i + 1]) {
@@ -264,49 +306,15 @@ export async function hydrate_target(target: Target): Promise<{
preloaded = initial_data.preloaded[i + 1];
}
return { Component, preloaded, segment };
return (props[`level${j}`] = { component, props: preloaded, segment, part: part.i });
}));
} catch (e) {
error = { statusCode: 500, message: e };
} catch (error) {
props.error = error;
props.status = 500;
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 };
return { redirect, props, branch };
}
function load_css(chunk: string) {
@@ -338,4 +346,4 @@ export function load_component(component: ComponentLoader): Promise<{
function detach(node: Node) {
node.parentNode.removeChild(node);
}
}

View File

@@ -6,6 +6,7 @@ import {
scroll_history,
scroll_state,
select_target,
handle_error,
set_target,
uid,
set_uid,
@@ -34,10 +35,12 @@ export default function start(opts: {
history.replaceState({ id: uid }, '', href);
if (!initial_data.error) {
const target = select_target(new URL(location.href));
if (target) return navigate(target, uid, false, hash);
}
const url = new URL(location.href);
if (initial_data.error) return handle_error(url);
const target = select_target(url);
if (target) return navigate(target, uid, false, hash);
});
}
@@ -127,4 +130,4 @@ function handle_popstate(event: PopStateEvent) {
set_cid(uid);
history.replaceState({ id: cid }, '', location.href);
}
}
}

View File

@@ -9,7 +9,7 @@ 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';
import App from '@sapper/internal/App.svelte';
export function get_page_handler(
manifest: Manifest,
@@ -38,7 +38,7 @@ export function get_page_handler(
}
async function handle_page(page: Page, req: Req, res: Res, status = 200, error: Error | string = null) {
const isSWIndexHtml = req.path === '/service-worker-index.html';
const is_service_worker_index = req.path === '/service-worker-index.html';
const build_info: {
bundler: 'rollup' | 'webpack',
shimport: string | null,
@@ -52,7 +52,7 @@ export function get_page_handler(
// preload main.js and current route
// TODO detect other stuff we can preload? images, CSS, fonts?
let preloaded_chunks = Array.isArray(build_info.assets.main) ? build_info.assets.main : [build_info.assets.main];
if (!error && !isSWIndexHtml) {
if (!error && !is_service_worker_index) {
page.parts.forEach(part => {
if (!part) return;
@@ -145,14 +145,14 @@ export function get_page_handler(
path: req.path,
query: req.query,
params: {}
})
}, session)
: {};
match = error ? null : page.pattern.exec(req.path);
let toPreload = [root_preloaded];
if (!isSWIndexHtml) {
if (!is_service_worker_index) {
toPreload = toPreload.concat(page.parts.map(part => {
if (!part) return null;
@@ -193,58 +193,62 @@ export function get_page_handler(
const segments = req.path.split('/').filter(Boolean);
const props = Object.assign({}, preloaded[0], {
child: {
// TODO make this less confusing
const layout_segments = [segments[0]];
let l = 1;
page.parts.forEach((part, i) => {
layout_segments[l] = segments[i + 1];
if (!part) return null;
l++;
});
const props = {
segments: layout_segments,
status: error ? status : 200,
error: error ? error instanceof Error ? error : { message: error } : null,
session: writable(session),
level0: {
props: preloaded[0]
},
level1: {
segment: segments[0],
props: {}
}
});
};
let level = props.child;
if (!isSWIndexHtml) {
if (!is_service_worker_index) {
let l = 1;
for (let i = 0; i < page.parts.length; i += 1) {
const part = page.parts[i];
if (!part) continue;
Object.assign(level, {
props[`level${l++}`] = {
component: part.component,
props: Object.assign({}, preloaded[i + 1])
});
level.props.child = <Props["child"]>{
segment: segments[i + 1],
props: {}
props: preloaded[i + 1] || {},
segment: segments[i]
};
level = level.props.child;
}
}
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 { html, head, css } = App.render(props);
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}`);
})
}),
error: error && try_serialize(props.error)
};
let script = `__SAPPER__={${[
error && `error:1`,
error && `error:${serialized.error},status:${status}`,
`baseUrl:"${req.baseUrl}"`,
serialized.preloaded && `preloaded:${serialized.preloaded}`,
serialized.session && `session:${serialized.session}`
@@ -326,12 +330,10 @@ export function get_page_handler(
return;
}
if (!server_routes.some(route => route.pattern.test(req.path))) {
for (const page of pages) {
if (page.pattern.test(req.path)) {
handle_page(page, req, res);
return;
}
for (const page of pages) {
if (page.pattern.test(req.path)) {
handle_page(page, req, res);
return;
}
}

View File

@@ -52,11 +52,6 @@ export default function middleware(opts: {
next();
},
fs.existsSync(path.join(build_dir, 'index.html')) && serve({
pathname: '/index.html',
cache_control: dev ? 'no-cache' : 'max-age=600'
}),
fs.existsSync(path.join(build_dir, 'service-worker.js')) && serve({
pathname: '/service-worker.js',
cache_control: 'no-cache, no-store, must-revalidate'
@@ -143,4 +138,4 @@ export function serve({ prefix, pathname, cache_control }: {
};
}
function noop(){}
function noop(){}

View File

@@ -1,15 +1,14 @@
import * as fs from 'fs';
import * as path from 'path';
import mkdirp from 'mkdirp';
import rimraf from 'rimraf';
import minify_html from './utils/minify_html';
import { create_compilers, create_main_manifests, create_manifest_data, create_serviceworker_manifest } from '../core';
import { create_compilers, create_app, create_manifest_data, create_serviceworker_manifest } from '../core';
import { copy_shimport } from './utils/copy_shimport';
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';
import { rimraf, mkdirp } from './utils/fs_utils';
type Opts = {
cwd?: string;
@@ -48,12 +47,12 @@ export async function build({
throw new Error(`Legacy builds are not supported for projects using webpack`);
}
rimraf.sync(path.join(output, '**/*'));
mkdirp.sync(output);
rimraf(output);
mkdirp(output);
copy_runtime(output);
rimraf.sync(path.join(dest, '**/*'));
mkdirp.sync(`${dest}/client`);
rimraf(dest);
mkdirp(`${dest}/client`);
copy_shimport(dest);
// minify src/template.html
@@ -72,7 +71,7 @@ export async function build({
const manifest_data = create_manifest_data(routes);
// create src/node_modules/@sapper/app.mjs and server.mjs
create_main_manifests({
create_app({
bundler,
manifest_data,
cwd,

View File

@@ -3,10 +3,8 @@ import * as fs from 'fs';
import * as http from 'http';
import * as child_process from 'child_process';
import * as ports from 'port-authority';
import mkdirp from 'mkdirp';
import rimraf from 'rimraf';
import { EventEmitter } from 'events';
import { create_manifest_data, create_main_manifests, create_compilers, create_serviceworker_manifest } from '../core';
import { create_manifest_data, create_app, create_compilers, create_serviceworker_manifest } from '../core';
import { Compiler, Compilers } from '../core/create_compilers';
import { CompileResult } from '../core/create_compilers/interfaces';
import Deferred from './utils/Deferred';
@@ -16,6 +14,7 @@ import { ManifestData, FatalEvent, ErrorEvent, ReadyEvent, InvalidEvent } from '
import read_template from '../core/read_template';
import { noop } from './utils/noop';
import { copy_runtime } from './utils/copy_runtime';
import { rimraf, mkdirp } from './utils/fs_utils';
type Opts = {
cwd?: string,
@@ -146,12 +145,12 @@ class Watcher extends EventEmitter {
const { cwd, src, dest, routes, output, static: static_files } = this.dirs;
rimraf.sync(path.join(output, '**/*'));
mkdirp.sync(output);
rimraf(output);
mkdirp(output);
copy_runtime(output);
rimraf.sync(dest);
mkdirp.sync(`${dest}/client`);
rimraf(dest);
mkdirp(`${dest}/client`);
if (this.bundler === 'rollup') copy_shimport(dest);
if (!this.dev_port) this.dev_port = await ports.find(10000);
@@ -163,7 +162,7 @@ class Watcher extends EventEmitter {
try {
manifest_data = create_manifest_data(routes);
create_main_manifests({
create_app({
bundler: this.bundler,
manifest_data,
dev: true,
@@ -191,7 +190,7 @@ class Watcher extends EventEmitter {
() => {
try {
const new_manifest_data = create_manifest_data(routes);
create_main_manifests({
create_app({
bundler: this.bundler,
manifest_data, // TODO is this right? not new_manifest_data?
dev: true,
@@ -200,9 +199,10 @@ class Watcher extends EventEmitter {
});
manifest_data = new_manifest_data;
} catch (err) {
} catch (error) {
this.emit('error', <ErrorEvent>{
message: err.message
type: 'manifest',
error
});
}
}
@@ -409,11 +409,11 @@ class Watcher extends EventEmitter {
}) {
compiler.oninvalid(invalid);
compiler.watch((err?: Error, result?: CompileResult) => {
if (err) {
compiler.watch((error?: Error, result?: CompileResult) => {
if (error) {
this.emit('error', <ErrorEvent>{
type: name,
message: err.message
error
});
} else {
this.emit('build', {

View File

@@ -1,6 +1,6 @@
import * as child_process from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as sander from 'sander';
import * as url from 'url';
import fetch from 'node-fetch';
import * as yootils from 'yootils';
@@ -9,6 +9,8 @@ import clean_html from './utils/clean_html';
import minify_html from './utils/minify_html';
import Deferred from './utils/Deferred';
import { noop } from './utils/noop';
import { parse as parseLinkHeader } from 'http-link-header';
import { rimraf, copy, mkdirp } from './utils/fs_utils';
type Opts = {
build_dir?: string,
@@ -21,6 +23,12 @@ type Opts = {
onfile?: ({ file, size, status }: { file: string, size: number, status: number }) => void;
};
type Ref = {
uri: string,
rel: string,
as: string
};
function resolve(from: string, to: string) {
return url.parse(url.resolve(from, to));
}
@@ -47,20 +55,15 @@ async function _export({
export_dir = path.resolve(cwd, export_dir, basepath);
// Prep output directory
sander.rimrafSync(export_dir);
rimraf(export_dir);
sander.copydirSync(static_files).to(export_dir);
sander.copydirSync(build_dir, 'client').to(export_dir, 'client');
copy(static_files, export_dir);
copy(path.join(build_dir, 'client'), path.join(export_dir, 'client'));
copy(path.join(build_dir, 'service-worker.js'), path.join(export_dir, 'service-worker.js'));
copy(path.join(build_dir, 'service-worker.js.map'), path.join(export_dir, 'service-worker.js.map'));
if (sander.existsSync(build_dir, 'service-worker.js')) {
sander.copyFileSync(build_dir, 'service-worker.js').to(export_dir, 'service-worker.js');
}
if (sander.existsSync(build_dir, 'service-worker.js.map')) {
sander.copyFileSync(build_dir, 'service-worker.js.map').to(export_dir, 'service-worker.js.map');
}
const port = await ports.find(3000);
const defaultPort = process.env.PORT ? parseInt(process.env.PORT) : 3000;
const port = await ports.find(defaultPort);
const protocol = 'http:';
const host = `localhost:${port}`;
@@ -85,8 +88,8 @@ async function _export({
const seen = new Set();
const saved = new Set();
function save(path: string, status: number, type: string, body: string) {
const { pathname } = resolve(origin, path);
function save(url: string, status: number, type: string, body: string) {
const { pathname } = resolve(origin, url);
let file = decodeURIComponent(pathname.slice(1));
if (saved.has(file)) return;
@@ -107,7 +110,9 @@ async function _export({
status
});
sander.writeFileSync(export_dir, file, body);
const export_file = path.join(export_dir, file);
mkdirp(path.dirname(export_file));
fs.writeFileSync(export_file, body);
}
proc.on('message', message => {
@@ -139,38 +144,48 @@ async function _export({
clearTimeout(the_timeout); // prevent it hanging at the end
let type = r.headers.get('Content-Type');
let body = await r.text();
const range = ~~(r.status / 100);
if (range === 2) {
if (type === 'text/html' && pathname !== '/service-worker-index.html') {
const cleaned = clean_html(body);
if (type === 'text/html') {
// parse link rel=preload headers and embed them in the HTML
let link = parseLinkHeader(r.headers.get('Link') || '');
link.refs.forEach((ref: Ref) => {
if (ref.rel === 'preload') {
body = body.replace('</head>',
`<link rel="preload" as=${JSON.stringify(ref.as)} href=${JSON.stringify(ref.uri)}></head>`)
}
});
if (pathname !== '/service-worker-index.html') {
const cleaned = clean_html(body);
const q = yootils.queue(8);
let promise;
const q = yootils.queue(8);
const base_match = /<base ([\s\S]+?)>/m.exec(cleaned);
const base_href = base_match && get_href(base_match[1]);
const base = resolve(url.href, base_href);
const base_match = /<base ([\s\S]+?)>/m.exec(cleaned);
const base_href = base_match && get_href(base_match[1]);
const base = resolve(url.href, base_href);
let match;
let pattern = /<a ([\s\S]+?)>/gm;
let match;
let pattern = /<a ([\s\S]+?)>/gm;
while (match = pattern.exec(cleaned)) {
const attrs = match[1];
const href = get_href(attrs);
while (match = pattern.exec(cleaned)) {
const attrs = match[1];
const href = get_href(attrs);
if (href) {
const url = resolve(base.href, href);
if (href) {
const url = resolve(base.href, href);
if (url.protocol === protocol && url.host === host) {
promise = q.add(() => handle(url));
if (url.protocol === protocol && url.host === host) {
q.add(() => handle(url));
}
}
}
}
await promise;
await q.close();
}
}
}
@@ -197,6 +212,6 @@ async function _export({
}
function get_href(attrs: string) {
const match = /href\s*=\s*(?:"(.*?)"|'(.+?)'|([^\s>]+))/.exec(attrs);
return match[1] || match[2] || match[3];
}
const match = /href\s*=\s*(?:"(.*?)"|'(.*?)'|([^\s>]*))/.exec(attrs);
return match && (match[1] || match[2] || match[3]);
}

View File

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

46
src/api/utils/fs_utils.ts Normal file
View File

@@ -0,0 +1,46 @@
import * as fs from 'fs';
import * as path from 'path';
export function mkdirp(dir: string) {
const parent = path.dirname(dir);
if (parent === dir) return;
mkdirp(parent);
try {
fs.mkdirSync(dir);
} catch (err) {
// ignore
}
}
export function rimraf(thing: string) {
if (!fs.existsSync(thing)) return;
const stats = fs.statSync(thing);
if (stats.isDirectory()) {
fs.readdirSync(thing).forEach(file => {
rimraf(path.join(thing, file));
});
fs.rmdirSync(thing);
} else {
fs.unlinkSync(thing);
}
}
export function copy(from: string, to: string) {
if (!fs.existsSync(from)) return;
const stats = fs.statSync(from);
if (stats.isDirectory()) {
fs.readdirSync(from).forEach(file => {
copy(path.join(from, file), path.join(to, file));
});
} else {
mkdirp(path.dirname(to));
fs.writeFileSync(to, fs.readFileSync(from));
}
}

View File

@@ -89,8 +89,16 @@ prog.command('dev')
});
watcher.on('error', (event: ErrorEvent) => {
console.log(colors.red(`${event.type}`));
console.log(colors.red(event.message));
const { type, error } = event;
console.log(colors.bold().red(`${type}`));
if (error.loc && error.loc.file) {
console.log(colors.bold(`${path.relative(process.cwd(), error.loc.file)} (${error.loc.line}:${error.loc.column})`));
}
console.log(colors.red(event.error.message));
if (error.frame) console.log(error.frame);
});
watcher.on('fatal', (event: FatalEvent) => {
@@ -103,7 +111,7 @@ prog.command('dev')
console.log(colors.bold().red(`${event.type}`));
event.errors.filter(e => !e.duplicate).forEach(error => {
if (error.file) console.log(colors.bold()(error.file));
if (error.file) console.log(colors.bold(error.file));
console.log(error.message);
});
@@ -115,7 +123,7 @@ prog.command('dev')
console.log(colors.bold().yellow(`${event.type}`));
event.warnings.filter(e => !e.duplicate).forEach(warning => {
if (warning.file) console.log(colors.bold()(warning.file));
if (warning.file) console.log(colors.bold(warning.file));
console.log(warning.message);
});
@@ -268,12 +276,12 @@ async function _build(
oncompile: event => {
let banner = `built ${event.type}`;
let c = colors.cyan;
let c = (txt: string) => colors.cyan(txt);
const { warnings } = event.result;
if (warnings.length > 0) {
banner += ` with ${warnings.length} ${warnings.length === 1 ? 'warning' : 'warnings'}`;
c = colors.yellow;
c = (txt: string) => colors.cyan(txt);
}
console.log();
@@ -284,4 +292,4 @@ async function _build(
console.log(event.result.print());
}
});
}
}

View File

@@ -1,3 +1,3 @@
export * from './core/create_manifests';
export * from './core/create_app';
export { default as create_compilers } from './core/create_compilers/index';
export { default as create_manifest_data } from './core/create_manifest_data';

View File

@@ -3,7 +3,7 @@ import * as path from 'path';
import { posixify, stringify, walk, write_if_changed } from '../utils';
import { Page, PageComponent, ManifestData } from '../interfaces';
export function create_main_manifests({
export function create_app({
bundler,
manifest_data,
dev_port,
@@ -31,8 +31,11 @@ export function create_main_manifests({
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);
const app = generate_app(manifest_data, path_to_routes);
write_if_changed(`${output}/internal/manifest-client.mjs`, client_manifest);
write_if_changed(`${output}/internal/manifest-server.mjs`, server_manifest);
write_if_changed(`${output}/internal/App.svelte`, app);
}
export function create_serviceworker_manifest({ manifest_data, output, client_files, static_files }: {
@@ -41,7 +44,7 @@ export function create_serviceworker_manifest({ manifest_data, output, client_fi
client_files: string[];
static_files: string;
}) {
let files: string[] = ['/service-worker-index.html'];
let files: string[] = ['service-worker-index.html'];
if (fs.existsSync(static_files)) {
files = files.concat(walk(static_files));
@@ -129,7 +132,7 @@ function generate_client_manifest(
// 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)}';
export { default as ErrorComponent } from '${stringify(get_file(path_to_routes, manifest_data.error), false)}';
export const ignore = [${server_routes_to_ignore.map(route => route.pattern).join(', ')}];
@@ -159,7 +162,7 @@ function generate_server_manifest(
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`))};`
`import error from ${stringify(get_file(path_to_routes, manifest_data.error))};`
);
const component_lookup: Record<string, number> = {};
@@ -228,15 +231,61 @@ function generate_server_manifest(
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))
.replace('__SRC__DIR__', JSON.stringify(src_dir))
.replace('__DEV__', dev ? 'true' : 'false')
.replace(/const manifest = __MANIFEST__;/, code);
function generate_app(manifest_data: ManifestData, path_to_routes: string) {
// TODO remove default layout altogether
const max_depth = Math.max(...manifest_data.pages.map(page => page.parts.filter(Boolean).length));
const levels = [];
for (let i = 0; i < max_depth; i += 1) {
levels.push(i + 1);
}
let l = max_depth;
let pyramid = `<svelte:component this={level${l}.component} {...level${l}.props}/>`;
while (l-- > 1) {
pyramid = `
<svelte:component this={level${l}.component} segment={segments[${l}]} {...level${l}.props}>
{#if level${l + 1}}
${pyramid.replace(/\n/g, '\n\t\t\t\t\t')}
{/if}
</svelte:component>
`.replace(/^\t\t\t/gm, '').trim();
}
return `
<!-- This file is generated by Sapper — do not edit it! -->
<script>
import { setContext } from 'svelte';
import { CONTEXT_KEY } from './shared';
import Layout from '${get_file(path_to_routes, manifest_data.root)}';
import Error from '${get_file(path_to_routes, manifest_data.error)}';
export let session;
export let error;
export let status;
export let segments;
export let level0;
${levels.map(l => `export let level${l} = null;`).join('\n\t\t\t')}
setContext(CONTEXT_KEY, session);
</script>
<Layout segment={segments[0]} {...level0.props}>
{#if error}
<Error {error} {status}/>
{:else}
${pyramid.replace(/\n/g, '\n\t\t\t\t')}
{/if}
</Layout>
`.replace(/^\t\t/gm, '').trim();
}
function get_file(path_to_routes: string, component: PageComponent) {
if (component.default) return `./layout.html`;
if (component.default) return `./${component.type}.svelte`;
return posixify(`${path_to_routes}/${component.file}`);
}

View File

@@ -1,6 +1,6 @@
import format_messages from 'webpack-format-messages';
import { CompileResult, BuildInfo, CompileError, Chunk, CssFile } from './interfaces';
import { ManifestData, Dirs } from '../../interfaces';
import { ManifestData, Dirs, PageComponent } from '../../interfaces';
const locPattern = /\((\d+):(\d+)\)$/;
@@ -66,12 +66,15 @@ export default class WebpackResult implements CompileResult {
assets: this.assets,
css: {
main: extract_css(this.assets.main),
chunks: Object
.keys(this.assets)
.filter(chunkName => chunkName !== 'main')
.reduce((chunks: { [key: string]: string }, chukName) => {
const assets = this.assets[chukName];
chunks[chukName] = extract_css(assets);
chunks: manifest_data.components
.reduce((chunks: Record<string, string[]>, component: PageComponent) => {
const css_dependencies = [];
const css = extract_css(this.assets[component.name]);
if (css) css_dependencies.push(css);
chunks[component.file] = css_dependencies;
return chunks;
}, {})
}
@@ -81,4 +84,4 @@ export default class WebpackResult implements CompileResult {
print() {
return this.stats.toString({ colors: true });
}
}
}

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, 'app.mjs');
const entry = path.resolve(dirs.src, 'client.js');
const entry_chunk = client_result.chunks.find(chunk => chunk.modules.indexOf(entry) !== -1);
const entry_chunk_dependencies: Set<Chunk> = new Set([entry_chunk]);

View File

@@ -4,6 +4,8 @@ import svelte from 'svelte/compiler';
import { Page, PageComponent, ServerRoute, ManifestData } from '../interfaces';
import { posixify, reserved_words } from '../utils';
const component_extensions = ['.svelte', '.html']; // TODO make this configurable (to include e.g. .svelte.md?)
export default function create_manifest_data(cwd: string): ManifestData {
// TODO remove in a future version
if (!fs.existsSync(cwd)) {
@@ -15,11 +17,8 @@ export default function create_manifest_data(cwd: string): ManifestData {
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');
const { vars } = svelte.compile(source.replace(/<style\b[^>]*>[^]*?<\/style>/g, ''), { generate: false });
return vars.some((variable: any) => variable.module && variable.export_name === 'preload');
} catch (err) {}
}
@@ -32,11 +31,20 @@ export default function create_manifest_data(cwd: string): ManifestData {
const default_layout: PageComponent = {
default: true,
type: 'layout',
name: '_default_layout',
file: null,
has_preload: false
};
const default_error: PageComponent = {
default: true,
type: 'error',
name: '_default_error',
file: null,
has_preload: false
};
function walk(
dir: string,
parent_segments: Part[][],
@@ -61,7 +69,7 @@ export default function create_manifest_data(cwd: string): ManifestData {
const parts = get_parts(segment);
const is_index = is_dir ? false : basename.startsWith('index.');
const is_page = ext === '.html';
const is_page = component_extensions.indexOf(ext) !== -1;
parts.forEach(part => {
if (/\]\[/.test(part.content)) {
@@ -75,6 +83,7 @@ export default function create_manifest_data(cwd: string): ManifestData {
return {
basename,
ext,
parts,
file: posixify(file),
is_dir,
@@ -121,12 +130,15 @@ export default function create_manifest_data(cwd: string): ManifestData {
params.push(...item.parts.filter(p => p.dynamic).map(p => p.content));
if (item.is_dir) {
const index = path.join(dir, item.basename, '_layout.html');
const ext = component_extensions.find((ext: string) => {
const index = path.join(dir, item.basename, `_layout${ext}`);
return fs.existsSync(index);
});
const component = fs.existsSync(index) && {
const component = ext && {
name: `${get_slug(item.file)}__layout`,
file: `${item.file}/_layout.html`,
has_preload: has_preload(`${item.file}/_layout.html`)
file: `${item.file}/_layout${ext}`,
has_preload: has_preload(`${item.file}/_layout${ext}`)
};
if (component) components.push(component);
@@ -142,29 +154,26 @@ export default function create_manifest_data(cwd: string): ManifestData {
}
else if (item.is_page) {
const is_index = item.basename === `index${item.ext}`;
const component = {
name: get_slug(item.file),
file: item.file,
has_preload: has_preload(item.file)
};
const parts = stack.concat({
component,
params
});
components.push(component);
if (item.basename === 'index.html') {
pages.push({
pattern: get_pattern(parent_segments, true),
parts
});
} else {
pages.push({
pattern: get_pattern(segments, true),
parts
});
}
const parts = (is_index && stack[stack.length - 1] === null)
? stack.slice(0, -1).concat({ component, params })
: stack.concat({ component, params })
const page = {
pattern: get_pattern(is_index ? parent_segments : segments, true),
parts
};
pages.push(page);
}
else {
@@ -178,15 +187,24 @@ export default function create_manifest_data(cwd: string): ManifestData {
});
}
const root_file = path.join(cwd, '_layout.html');
const root = fs.existsSync(root_file)
const root_ext = component_extensions.find(ext => fs.existsSync(path.join(cwd, `_layout${ext}`)));
const root = root_ext
? {
name: 'main',
file: '_layout.html',
has_preload: has_preload('_layout.html')
file: `_layout${root_ext}`,
has_preload: has_preload(`_layout${root_ext}`)
}
: default_layout;
const error_ext = component_extensions.find(ext => fs.existsSync(path.join(cwd, `_error${ext}`)));
const error = error_ext
? {
name: 'error',
file: `_error${error_ext}`,
has_preload: has_preload(`_error${error_ext}`)
}
: default_error;
walk(cwd, [], [], []);
// check for clashes
@@ -217,6 +235,7 @@ export default function create_manifest_data(cwd: string): ManifestData {
return {
root,
error,
components,
pages,
server_routes
@@ -324,4 +343,4 @@ function get_pattern(segments: Part[][], add_trailing_slash: boolean) {
}).join('') +
(add_trailing_slash ? '\\\/?$' : '$')
);
}
}

View File

@@ -27,6 +27,7 @@ export type WritableStore<T> = {
export type PageComponent = {
default?: boolean;
type?: string;
name: string;
file: string;
has_preload: boolean;
@@ -55,6 +56,7 @@ export type Dirs = {
export type ManifestData = {
root: PageComponent;
error: PageComponent;
components: PageComponent[];
pages: Page[];
server_routes: ServerRoute[];
@@ -67,7 +69,7 @@ export type ReadyEvent = {
export type ErrorEvent = {
type: string;
message: string;
error: Error;
};
export type FatalEvent = {

View File

@@ -0,0 +1,3 @@
<h1>A page</h1>
<a href="dirs/foo/xyz">same segment</a>

View File

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

View File

@@ -0,0 +1 @@
<h1>B page</h1>

View File

@@ -0,0 +1,2 @@
<h1>foo</h1>
<a href="dirs/bar">bar</a>

View File

@@ -0,0 +1,8 @@
export function get(req, res, next) {
if (req.headers.accept === 'application/json') {
res.end('{"json":true}');
return;
}
next();
}

View File

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

View File

@@ -8,6 +8,25 @@ import { wait } from '../../utils';
declare let deleted: { id: number };
declare let el: any;
function get(url: string, opts?: any): Promise<{ headers: Record<string, string>, body: string }> {
return new Promise((fulfil, reject) => {
const req = http.get(url, opts || {}, res => {
res.on('error', reject);
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => {
fulfil({
headers: res.headers as Record<string, string>,
body
});
});
});
req.on('error', reject);
});
}
describe('basics', function() {
this.timeout(10000);
@@ -116,38 +135,25 @@ describe('basics', function() {
assert.equal(requests[1], `${base}/b.json`);
});
// TODO equivalent test for a webpack app
it('sets Content-Type, Link...modulepreload, and Cache-Control headers', () => {
return new Promise((fulfil, reject) => {
const req = http.get(base, res => {
try {
const { headers } = res;
it('sets Content-Type, Link...modulepreload, and Cache-Control headers', async () => {
const { headers } = await get(base);
assert.equal(
headers['content-type'],
'text/html'
);
assert.equal(
headers['content-type'],
'text/html'
);
assert.equal(
headers['cache-control'],
'max-age=600'
);
assert.equal(
headers['cache-control'],
'max-age=600'
);
// TODO preload more than just the entry point
const regex = /<\/client\/client\.\w+\.js>;rel="modulepreload"/;
const link = <string>headers['link'];
// TODO preload more than just the entry point
const regex = /<\/client\/client\.\w+\.js>;rel="modulepreload"/;
const link = <string>headers['link'];
assert.ok(regex.test(link), link);
fulfil();
} catch (err) {
reject(err);
}
});
req.on('error', reject);
});
assert.ok(regex.test(link), link);
});
it('calls a delete handler', async () => {
@@ -264,4 +270,40 @@ describe('basics', function() {
const html = String(await page.evaluate(() => document.body.innerHTML));
assert.equal(html.indexOf('%sapper'), -1);
});
it('navigates between routes with empty parts', async () => {
await page.goto(`${base}/dirs/foo`);
await start();
assert.equal(await title(), 'foo');
await page.click('[href="dirs/bar"]');
await wait(50);
assert.equal(await title(), 'bar');
});
it('navigates between dynamic routes with same segments', async () => {
await page.goto(`${base}/dirs/bar/xyz`);
await start();
assert.equal(await title(), 'A page');
await page.click('[href="dirs/foo/xyz"]');
await wait(50);
assert.equal(await title(), 'B page');
});
it('runs server route handlers before page handlers, if they match', async () => {
const json = await get(`${base}/middleware`, {
headers: {
'Accept': 'application/json'
}
});
assert.equal(json.body, '{"json":true}');
const html = await get(`${base}/middleware`);
assert.ok(html.body.indexOf('<h1>HTML</h1>') !== -1);
});
});

View File

@@ -4,7 +4,7 @@
export let Title;
onMount(() => {
import('./_components/Title.html').then(mod => {
import('./_components/Title.svelte').then(mod => {
Title = mod.default;
});
});

View File

@@ -0,0 +1,17 @@
<script>
import { onMount, onDestroy } from 'svelte';
export let status, error = {};
let mounted = false;
onMount(() => {
mounted = 'success';
})
</script>
<h1>{status}</h1>
<h2>{mounted}</h2>
<p>{error.message}</p>

View File

@@ -112,6 +112,17 @@ describe('errors', function() {
);
});
it('execute error page hooks', async () => {
await page.goto(`${base}/some-throw-page`);
await start();
await wait(50);
assert.equal(
await page.$eval('h2', node => node.textContent),
'success'
);
})
it('does not serve error page for async non-page error', async () => {
await page.goto(`${base}/async-throw.json`);
@@ -134,4 +145,4 @@ describe('errors', function() {
await wait(50);
assert.equal(await title(), 'No error here');
});
});
});

View File

@@ -0,0 +1,9 @@
import * as sapper from '@sapper/app';
window.start = () => sapper.start({
target: document.querySelector('#sapper')
});
window.prefetchRoutes = () => sapper.prefetchRoutes();
window.prefetch = href => sapper.prefetch(href);
window.goto = href => sapper.goto(href);

View File

@@ -0,0 +1,13 @@
<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 let post;
</script>
<h1>{post.title}</h1>

View File

@@ -0,0 +1,19 @@
import posts from './_posts.js';
export function get(req, res) {
const post = posts.find(post => post.slug === req.params.slug);
if (post) {
res.writeHead(200, {
'Content-Type': 'application/json'
});
res.end(JSON.stringify(post));
} else {
res.writeHead(404, {
'Content-Type': 'application/json'
});
res.end(JSON.stringify({ message: 'not found' }));
}
}

View File

@@ -0,0 +1,5 @@
export default [
{ slug: 'foo', title: 'once upon a foo' },
{ slug: 'bar', title: 'a bar is born' },
{ slug: 'baz', title: 'bazzily ever after' }
];

View File

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

View File

@@ -0,0 +1,9 @@
import posts from './_posts.js';
export function get(req, res) {
res.writeHead(200, {
'Content-Type': 'application/json'
});
res.end(JSON.stringify(posts));
}

View File

@@ -0,0 +1,15 @@
import sirv from 'sirv';
import polka from 'polka';
import * as sapper from '@sapper/server';
const { PORT, NODE_ENV } = process.env;
const dev = NODE_ENV === 'development';
polka()
.use(
sirv('static', { dev }),
sapper.middleware()
)
.listen(PORT, err => {
if (err) console.log('error', err);
});

View File

@@ -0,0 +1,82 @@
import * as sapper from '@sapper/service-worker';
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 = sapper.shell.concat(sapper.files);
const cached = new Set(to_cache);
self.addEventListener('install', event => {
event.waitUntil(
caches
.open(ASSETS)
.then(cache => cache.addAll(to_cache))
.then(() => {
self.skipWaiting();
})
);
});
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(async keys => {
// delete old caches
for (const key of keys) {
if (key !== ASSETS) await caches.delete(key);
}
self.clients.claim();
})
);
});
self.addEventListener('fetch', event => {
if (event.request.method !== 'GET') return;
const url = new URL(event.request.url);
// don't try to handle e.g. data: URIs
if (!url.protocol.startsWith('http')) return;
// ignore dev server requests
if (url.hostname === self.location.hostname && url.port !== self.location.port) return;
// always serve assets and webpack-generated files from cache
if (url.host === self.location.host && cached.has(url.pathname)) {
event.respondWith(caches.match(event.request));
return;
}
// for pages, you might want to serve a shell `index.html` file,
// 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;
}
*/
if (event.request.cache === 'only-if-cached') return;
// for everything else, try the network first, falling back to
// cache if the user is offline. (If the pages never change, you
// might prefer a cache-first approach to a network-first one.)
event.respondWith(
caches
.open(`offline${sapper.timestamp}`)
.then(async cache => {
try {
const response = await fetch(event.request);
cache.put(event.request, response.clone());
return response;
} catch(err) {
const response = await cache.match(event.request);
if (response) return response;
throw err;
}
})
);
});

View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset='utf-8'>
%sapper.base%
%sapper.styles%
%sapper.head%
</head>
<body>
<div id='sapper'>%sapper.html%</div>
%sapper.scripts%
</body>
</html>

View File

@@ -0,0 +1,3 @@
body {
font-family: 'Comic Sans MS';
}

View File

@@ -0,0 +1,19 @@
import * as assert from 'assert';
import * as api from '../../../api';
import * as fs from 'fs';
describe('export-webpack', function() {
this.timeout(10000);
// hooks
before(async () => {
await api.build({ cwd: __dirname, bundler: 'webpack' });
await api.export({ cwd: __dirname, bundler: 'webpack' });
});
it('injects <link rel=preload> tags', () => {
const index = fs.readFileSync(`${__dirname}/__sapper__/export/index.html`, 'utf8');
assert.ok(/rel=preload/.test(index));
});
});

View File

@@ -0,0 +1,73 @@
const webpack = require('webpack');
const config = require('../../../config/webpack.js');
const mode = process.env.NODE_ENV;
const dev = mode === 'development';
module.exports = {
client: {
entry: config.client.entry(),
output: config.client.output(),
resolve: {
extensions: ['.mjs', '.js', '.json', '.html', '.svelte'],
mainFields: ['svelte', 'module', 'browser', 'main']
},
module: {
rules: [
{
test: /\.(html|svelte)$/,
use: {
loader: 'svelte-loader',
options: {
dev,
hydratable: true,
hotReload: true
}
}
}
]
},
mode,
plugins: [
dev && new webpack.HotModuleReplacementPlugin(),
new webpack.DefinePlugin({
'process.browser': true,
'process.env.NODE_ENV': JSON.stringify(mode)
}),
].filter(Boolean),
devtool: dev ? 'inline-source-map' : 'source-map'
},
server: {
entry: config.server.entry(),
output: config.server.output(),
target: 'node',
resolve: {
extensions: ['.mjs', '.js', '.json', '.html', '.svelte'],
mainFields: ['svelte', 'module', 'browser', 'main']
},
module: {
rules: [
{
test: /\.(html|svelte)$/,
use: {
loader: 'svelte-loader',
options: {
css: false,
generate: 'ssr',
dev
}
}
}
]
},
mode: process.env.NODE_ENV
},
serviceworker: {
entry: config.serviceworker.entry(),
output: config.serviceworker.output(),
mode: process.env.NODE_ENV,
devtool: 'sourcemap'
}
};

View File

@@ -0,0 +1,9 @@
<h1>Great success!</h1>
<a href="blog">blog</a>
<a href="">empty anchor</a>
<a href=''>empty anchor #2</a>
<a href=>empty anchor #3</a>
<a href= >empty anchor #4</a>
<a href>empty anchor #5</a>
<a>empty anchor #6</a>

View File

@@ -12,10 +12,10 @@
import { page } from '@sapper/app';
export let count;
export let child;
export let segment;
</script>
<span>y: {$page.params.y} {count}</span>
<svelte:component this={child.component} {...child.props}/>
<slot></slot>
<span>child segment: {child.segment}</span>
<span>child segment: {segment}</span>

View File

@@ -28,7 +28,7 @@ describe('layout', function() {
await page.goto(`${base}/foo/bar/baz`);
const text1 = String(await page.evaluate(() => document.querySelector('#sapper').textContent));
assert.deepEqual(text1.split('\n').filter(Boolean).map(str => str.trim()), [
assert.deepEqual(text1.split('\n').map(str => str.trim()).filter(Boolean), [
'y: bar 1',
'z: baz 1',
'click me',
@@ -37,7 +37,7 @@ describe('layout', function() {
await start();
const text2 = String(await page.evaluate(() => document.querySelector('#sapper').textContent));
assert.deepEqual(text2.split('\n').filter(Boolean).map(str => str.trim()), [
assert.deepEqual(text2.split('\n').map(str => str.trim()).filter(Boolean), [
'y: bar 1',
'z: baz 1',
'click me',
@@ -48,7 +48,7 @@ describe('layout', function() {
await wait(50);
const text3 = String(await page.evaluate(() => document.querySelector('#sapper').textContent));
assert.deepEqual(text3.split('\n').filter(Boolean).map(str => str.trim()), [
assert.deepEqual(text3.split('\n').map(str => str.trim()).filter(Boolean), [
'y: bar 1',
'z: qux 2',
'click me',

View File

@@ -8,13 +8,16 @@
<script>
import { preloading } from '@sapper/app';
import { setContext } from 'svelte';
export let child;
export let rootPreloadFunctionRan;
setContext('x', { rootPreloadFunctionRan });
</script>
{#if $preloading}
<progress class='preloading-progress' value=0.5/>
{/if}
<svelte:component this={child.component} {rootPreloadFunctionRan} {...child.props}/>
<slot></slot>

View File

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

View File

@@ -0,0 +1,5 @@
<script context="module">
export function preload() {}
</script>
<h1>Page loaded</h1>

View File

@@ -1 +0,0 @@
<h1>root preload function ran: {rootPreloadFunctionRan}</h1>

View File

@@ -0,0 +1,6 @@
<script>
import { getContext } from 'svelte';
const { rootPreloadFunctionRan } = getContext('x');
</script>
<h1>root preload function ran: {rootPreloadFunctionRan}</h1>

View File

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

View File

@@ -37,6 +37,15 @@ describe('preloading', function() {
assert.equal(await title(), 'true');
});
it('prevent crash if preload return nothing', async () => {
await page.goto(`${base}/preload-nothing`);
await start();
await wait(50);
assert.equal(await title(), 'Page loaded');
});
it('bails on custom classes returned from preload', async () => {
await page.goto(`${base}/preload-values/custom-class`);

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