mirror of
https://github.com/kevin-DL/sapper.git
synced 2026-01-14 03:54:46 +00:00
Compare commits
66 Commits
v0.26.0-al
...
node-12
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0588ed9ab | ||
|
|
01dd08849a | ||
|
|
534a96214e | ||
|
|
9725767fe2 | ||
|
|
90ec61f14d | ||
|
|
4f9919e95c | ||
|
|
b089ca42ff | ||
|
|
6cb4030b2b | ||
|
|
a89f7b01bb | ||
|
|
96a068245b | ||
|
|
0862d0e2c8 | ||
|
|
a26f8600c1 | ||
|
|
f9d1dc5d3f | ||
|
|
52c4106d2c | ||
|
|
0fd332135e | ||
|
|
9bb8bfa884 | ||
|
|
01c0097acb | ||
|
|
dcf726a89b | ||
|
|
9e60a71cf5 | ||
|
|
3a9d457389 | ||
|
|
1e9cd84854 | ||
|
|
d2cda4b6c0 | ||
|
|
0dd2d2eb4a | ||
|
|
6bf3dd04dd | ||
|
|
6d5aa9a35d | ||
|
|
7be7e1eb9f | ||
|
|
ca7973465b | ||
|
|
f7c88df3be | ||
|
|
74c66b784f | ||
|
|
9e9bd10333 | ||
|
|
8858301fed | ||
|
|
9540383796 | ||
|
|
b5edf0edd5 | ||
|
|
6dad750942 | ||
|
|
eee9d21900 | ||
|
|
55505571f8 | ||
|
|
4fe3c96c2d | ||
|
|
411e2594af | ||
|
|
e0de230e13 | ||
|
|
c637687922 | ||
|
|
57fe5bdfa2 | ||
|
|
b2b476abb1 | ||
|
|
ad0ebb8a69 | ||
|
|
130eafbd0a | ||
|
|
9d2ce6d852 | ||
|
|
a476d21c9b | ||
|
|
30b4b6660b | ||
|
|
cfd10c6f61 | ||
|
|
82a4973943 | ||
|
|
0609a92f3a | ||
|
|
37780656fd | ||
|
|
351ab13d29 | ||
|
|
795da23418 | ||
|
|
1f1211b7b4 | ||
|
|
acafeac1cc | ||
|
|
82e637ea7c | ||
|
|
14ace57612 | ||
|
|
84a0ae562f | ||
|
|
8870b58766 | ||
|
|
54506c1eb6 | ||
|
|
4f6b2dcb7c | ||
|
|
0a87204593 | ||
|
|
720cf8a859 | ||
|
|
ca034d0857 | ||
|
|
96b9d19715 | ||
|
|
293da8bcd1 |
@@ -3,7 +3,6 @@ sudo: false
|
|||||||
language: node_js
|
language: node_js
|
||||||
|
|
||||||
node_js:
|
node_js:
|
||||||
- "6"
|
|
||||||
- "stable"
|
- "stable"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ Sapper is a framework for building high-performance universal web apps. [Read th
|
|||||||
## Get started
|
## Get started
|
||||||
|
|
||||||
Clone the [starter project template](https://github.com/sveltejs/sapper-template) with [degit](https://github.com/rich-harris/degit)...
|
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
|
```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...
|
...then install dependencies and start the dev server...
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ build: off
|
|||||||
environment:
|
environment:
|
||||||
matrix:
|
matrix:
|
||||||
# node.js
|
# node.js
|
||||||
- nodejs_version: 10.5
|
- nodejs_version: 11
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- ps: Install-Product node $env:nodejs_version
|
- ps: Install-Product node $env:nodejs_version
|
||||||
|
|||||||
2504
package-lock.json
generated
2504
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
21
package.json
21
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sapper",
|
"name": "sapper",
|
||||||
"version": "0.26.0-alpha.3",
|
"version": "0.26.0-alpha.12",
|
||||||
"description": "Military-grade apps, engineered by Svelte",
|
"description": "Military-grade apps, engineered by Svelte",
|
||||||
"bin": {
|
"bin": {
|
||||||
"sapper": "./sapper"
|
"sapper": "./sapper"
|
||||||
@@ -19,18 +19,15 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"html-minifier": "^3.5.21",
|
"html-minifier": "^3.5.21",
|
||||||
|
"http-link-header": "^1.0.2",
|
||||||
"shimport": "0.0.14",
|
"shimport": "0.0.14",
|
||||||
"source-map-support": "^0.5.10",
|
|
||||||
"sourcemap-codec": "^1.4.4",
|
"sourcemap-codec": "^1.4.4",
|
||||||
"string-hash": "^1.1.3",
|
"string-hash": "^1.1.3"
|
||||||
"tslib": "^1.9.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/mkdirp": "^0.5.2",
|
|
||||||
"@types/mocha": "^5.2.5",
|
"@types/mocha": "^5.2.5",
|
||||||
"@types/node": "^10.12.21",
|
"@types/node": "^10.12.21",
|
||||||
"@types/puppeteer": "^1.11.3",
|
"@types/puppeteer": "^1.11.3",
|
||||||
"@types/rimraf": "^2.0.2",
|
|
||||||
"agadoo": "^1.0.1",
|
"agadoo": "^1.0.1",
|
||||||
"cheap-watch": "^1.0.2",
|
"cheap-watch": "^1.0.2",
|
||||||
"cookie": "^0.3.1",
|
"cookie": "^0.3.1",
|
||||||
@@ -38,7 +35,6 @@
|
|||||||
"eslint": "^5.12.1",
|
"eslint": "^5.12.1",
|
||||||
"eslint-plugin-import": "^2.16.0",
|
"eslint-plugin-import": "^2.16.0",
|
||||||
"kleur": "^3.0.1",
|
"kleur": "^3.0.1",
|
||||||
"mkdirp": "^0.5.1",
|
|
||||||
"mocha": "^5.2.0",
|
"mocha": "^5.2.0",
|
||||||
"node-fetch": "^2.3.0",
|
"node-fetch": "^2.3.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
@@ -47,7 +43,6 @@
|
|||||||
"pretty-bytes": "^5.1.0",
|
"pretty-bytes": "^5.1.0",
|
||||||
"puppeteer": "^1.12.0",
|
"puppeteer": "^1.12.0",
|
||||||
"require-relative": "^0.8.7",
|
"require-relative": "^0.8.7",
|
||||||
"rimraf": "^2.6.3",
|
|
||||||
"rollup": "^1.1.2",
|
"rollup": "^1.1.2",
|
||||||
"rollup-plugin-commonjs": "^9.2.0",
|
"rollup-plugin-commonjs": "^9.2.0",
|
||||||
"rollup-plugin-json": "^3.1.0",
|
"rollup-plugin-json": "^3.1.0",
|
||||||
@@ -55,16 +50,12 @@
|
|||||||
"rollup-plugin-replace": "^2.1.0",
|
"rollup-plugin-replace": "^2.1.0",
|
||||||
"rollup-plugin-string": "^2.0.2",
|
"rollup-plugin-string": "^2.0.2",
|
||||||
"rollup-plugin-sucrase": "^2.1.0",
|
"rollup-plugin-sucrase": "^2.1.0",
|
||||||
"rollup-plugin-svelte": "^5.0.1",
|
"rollup-plugin-svelte": "^5.0.3",
|
||||||
"rollup-plugin-typescript": "^1.0.0",
|
|
||||||
"sade": "^1.4.2",
|
"sade": "^1.4.2",
|
||||||
"sander": "^0.6.0",
|
|
||||||
"sirv": "^0.2.2",
|
"sirv": "^0.2.2",
|
||||||
"sucrase": "^3.9.5",
|
"sucrase": "^3.9.5",
|
||||||
"svelte": "^3.0.0-alpha27",
|
"svelte": "^3.0.0-beta.11",
|
||||||
"svelte-loader": "^2.12.0",
|
"svelte-loader": "^2.13.3",
|
||||||
"ts-node": "^8.0.2",
|
|
||||||
"typescript": "^3.3.1",
|
|
||||||
"webpack": "^4.29.0",
|
"webpack": "^4.29.0",
|
||||||
"webpack-format-messages": "^2.0.5",
|
"webpack-format-messages": "^2.0.5",
|
||||||
"yootils": "0.0.14"
|
"yootils": "0.0.14"
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import { builtinModules } from 'module';
|
|||||||
const external = [].concat(
|
const external = [].concat(
|
||||||
Object.keys(pkg.dependencies),
|
Object.keys(pkg.dependencies),
|
||||||
Object.keys(process.binding('natives')),
|
Object.keys(process.binding('natives')),
|
||||||
'sapper/core.js'
|
'sapper/core.js',
|
||||||
|
'svelte/compiler'
|
||||||
);
|
);
|
||||||
|
|
||||||
function template(kind, external) {
|
function template(kind, external) {
|
||||||
@@ -38,7 +39,7 @@ function template(kind, external) {
|
|||||||
|
|
||||||
export default [
|
export default [
|
||||||
template('app', id => /^(svelte\/?|@sapper\/)/.test(id)),
|
template('app', id => /^(svelte\/?|@sapper\/)/.test(id)),
|
||||||
template('server', id => builtinModules.includes(id)),
|
template('server', id => /^(svelte\/?|@sapper\/)/.test(id) || builtinModules.includes(id)),
|
||||||
|
|
||||||
{
|
{
|
||||||
input: [
|
input: [
|
||||||
|
|||||||
7
runtime/internal/error.svelte
Normal file
7
runtime/internal/error.svelte
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<h1>{status}</h1>
|
||||||
|
|
||||||
|
<p>{error.message}</p>
|
||||||
|
|
||||||
|
{#if process.env.NODE_ENV === 'development'}
|
||||||
|
<pre>{error.stack}</pre>
|
||||||
|
{/if}
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svelte:component this={child.component} {...child.props}/>
|
|
||||||
1
runtime/internal/layout.svelte
Normal file
1
runtime/internal/layout.svelte
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<slot></slot>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { writable } from 'svelte/store.mjs';
|
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 { stores } from '@sapper/internal/shared';
|
||||||
import { Root, root_preload, ErrorComponent, ignore, components, routes } from '@sapper/internal/manifest-client';
|
import { Root, root_preload, ErrorComponent, ignore, components, routes } from '@sapper/internal/manifest-client';
|
||||||
import {
|
import {
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
ComponentLoader,
|
ComponentLoader,
|
||||||
ComponentConstructor,
|
ComponentConstructor,
|
||||||
Route,
|
Route,
|
||||||
|
Query,
|
||||||
Page
|
Page
|
||||||
} from './types';
|
} from './types';
|
||||||
import goto from './goto';
|
import goto from './goto';
|
||||||
@@ -80,6 +81,20 @@ export { _history as history };
|
|||||||
|
|
||||||
export const scroll_history: Record<string, ScrollPosition> = {};
|
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 {
|
export function select_target(url: URL): Target {
|
||||||
if (url.origin !== location.origin) return null;
|
if (url.origin !== location.origin) return null;
|
||||||
if (!url.pathname.startsWith(initial_data.baseUrl)) 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 route = routes[i];
|
||||||
|
|
||||||
const match = route.pattern.exec(path);
|
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 part = route.parts[route.parts.length - 1];
|
||||||
const params = part.params ? part.params(match) : {};
|
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() {
|
export function scroll_state() {
|
||||||
return {
|
return {
|
||||||
x: pageXOffset,
|
x: pageXOffset,
|
||||||
@@ -158,7 +193,7 @@ export async function navigate(target: Target, id: number, noscroll?: boolean, h
|
|||||||
|
|
||||||
if (hash) {
|
if (hash) {
|
||||||
// scroll is an element id (from a hash), we need to compute y.
|
// 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) {
|
if (deep_linked) {
|
||||||
scroll = {
|
scroll = {
|
||||||
@@ -180,8 +215,13 @@ async function render(redirect: Redirect, branch: any[], props: any, page: Page)
|
|||||||
stores.preloading.set(false);
|
stores.preloading.set(false);
|
||||||
|
|
||||||
if (root_component) {
|
if (root_component) {
|
||||||
root_component.props = props;
|
root_component.$set(props);
|
||||||
} else {
|
} else {
|
||||||
|
props.session = session;
|
||||||
|
props.level0 = {
|
||||||
|
props: await root_preloaded
|
||||||
|
};
|
||||||
|
|
||||||
// first load — remove SSR'd <head> contents
|
// first load — remove SSR'd <head> contents
|
||||||
const start = document.querySelector('#sapper-head-start');
|
const start = document.querySelector('#sapper-head-start');
|
||||||
const end = document.querySelector('#sapper-head-end');
|
const end = document.querySelector('#sapper-head-end');
|
||||||
@@ -192,13 +232,9 @@ async function render(redirect: Redirect, branch: any[], props: any, page: Page)
|
|||||||
detach(end);
|
detach(end);
|
||||||
}
|
}
|
||||||
|
|
||||||
root_component = new Sapper({
|
root_component = new App({
|
||||||
target,
|
target,
|
||||||
props: {
|
props,
|
||||||
Root,
|
|
||||||
props,
|
|
||||||
session
|
|
||||||
},
|
|
||||||
hydrate: true
|
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<{
|
export async function hydrate_target(target: Target): Promise<{
|
||||||
redirect?: Redirect;
|
redirect?: Redirect;
|
||||||
props?: any;
|
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 { route, page } = target;
|
||||||
const segments = page.path.split('/').filter(Boolean);
|
const segments = page.path.split('/').filter(Boolean);
|
||||||
|
|
||||||
let redirect: Redirect = null;
|
let redirect: Redirect = null;
|
||||||
let error: { statusCode: number, message: Error | string } = null;
|
|
||||||
|
const props = { error: null, status: 200, segments: [segments[0]] };
|
||||||
|
|
||||||
const preload_context = {
|
const preload_context = {
|
||||||
fetch: (url: string, opts?: any) => fetch(url, opts),
|
fetch: (url: string, opts?: any) => fetch(url, opts),
|
||||||
@@ -227,8 +264,9 @@ export async function hydrate_target(target: Target): Promise<{
|
|||||||
}
|
}
|
||||||
redirect = { statusCode, location };
|
redirect = { statusCode, location };
|
||||||
},
|
},
|
||||||
error: (statusCode: number, message: Error | string) => {
|
error: (status: number, error: Error | string) => {
|
||||||
error = { statusCode, message };
|
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 branch;
|
||||||
|
let l = 1;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
branch = await Promise.all(route.parts.map(async (part, i) => {
|
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;
|
if (!part) return null;
|
||||||
|
|
||||||
const segment = segments[i];
|
const j = l++;
|
||||||
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]);
|
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;
|
let preloaded;
|
||||||
if (ready || !initial_data.preloaded[i + 1]) {
|
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];
|
preloaded = initial_data.preloaded[i + 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
return { Component, preloaded, segment };
|
return (props[`level${j}`] = { component, props: preloaded, segment, part: part.i });
|
||||||
}));
|
}));
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
error = { statusCode: 500, message: e };
|
props.error = error;
|
||||||
|
props.status = 500;
|
||||||
branch = [];
|
branch = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (redirect) return { redirect };
|
return { redirect, props, branch };
|
||||||
|
|
||||||
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) {
|
function load_css(chunk: string) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
scroll_history,
|
scroll_history,
|
||||||
scroll_state,
|
scroll_state,
|
||||||
select_target,
|
select_target,
|
||||||
|
handle_error,
|
||||||
set_target,
|
set_target,
|
||||||
uid,
|
uid,
|
||||||
set_uid,
|
set_uid,
|
||||||
@@ -34,10 +35,12 @@ export default function start(opts: {
|
|||||||
|
|
||||||
history.replaceState({ id: uid }, '', href);
|
history.replaceState({ id: uid }, '', href);
|
||||||
|
|
||||||
if (!initial_data.error) {
|
const url = new URL(location.href);
|
||||||
const target = select_target(new URL(location.href));
|
|
||||||
if (target) return navigate(target, uid, false, hash);
|
if (initial_data.error) return handle_error(url);
|
||||||
}
|
|
||||||
|
const target = select_target(url);
|
||||||
|
if (target) return navigate(target, uid, false, hash);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { IGNORE } from '../constants';
|
|||||||
import { Manifest, Page, Props, Req, Res } from './types';
|
import { Manifest, Page, Props, Req, Res } from './types';
|
||||||
import { build_dir, dev, src_dir } from '@sapper/internal/manifest-server';
|
import { build_dir, dev, src_dir } from '@sapper/internal/manifest-server';
|
||||||
import { stores } from '@sapper/internal/shared';
|
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(
|
export function get_page_handler(
|
||||||
manifest: Manifest,
|
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) {
|
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: {
|
const build_info: {
|
||||||
bundler: 'rollup' | 'webpack',
|
bundler: 'rollup' | 'webpack',
|
||||||
shimport: string | null,
|
shimport: string | null,
|
||||||
@@ -52,7 +52,7 @@ export function get_page_handler(
|
|||||||
// preload main.js and current route
|
// preload main.js and current route
|
||||||
// TODO detect other stuff we can preload? images, CSS, fonts?
|
// 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];
|
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 => {
|
page.parts.forEach(part => {
|
||||||
if (!part) return;
|
if (!part) return;
|
||||||
|
|
||||||
@@ -145,14 +145,14 @@ export function get_page_handler(
|
|||||||
path: req.path,
|
path: req.path,
|
||||||
query: req.query,
|
query: req.query,
|
||||||
params: {}
|
params: {}
|
||||||
})
|
}, session)
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
match = error ? null : page.pattern.exec(req.path);
|
match = error ? null : page.pattern.exec(req.path);
|
||||||
|
|
||||||
|
|
||||||
let toPreload = [root_preloaded];
|
let toPreload = [root_preloaded];
|
||||||
if (!isSWIndexHtml) {
|
if (!is_service_worker_index) {
|
||||||
toPreload = toPreload.concat(page.parts.map(part => {
|
toPreload = toPreload.concat(page.parts.map(part => {
|
||||||
if (!part) return null;
|
if (!part) return null;
|
||||||
|
|
||||||
@@ -193,58 +193,62 @@ export function get_page_handler(
|
|||||||
|
|
||||||
const segments = req.path.split('/').filter(Boolean);
|
const segments = req.path.split('/').filter(Boolean);
|
||||||
|
|
||||||
const props = Object.assign({}, preloaded[0], {
|
// TODO make this less confusing
|
||||||
child: {
|
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],
|
segment: segments[0],
|
||||||
props: {}
|
props: {}
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
let level = props.child;
|
if (!is_service_worker_index) {
|
||||||
if (!isSWIndexHtml) {
|
let l = 1;
|
||||||
for (let i = 0; i < page.parts.length; i += 1) {
|
for (let i = 0; i < page.parts.length; i += 1) {
|
||||||
const part = page.parts[i];
|
const part = page.parts[i];
|
||||||
if (!part) continue;
|
if (!part) continue;
|
||||||
|
|
||||||
Object.assign(level, {
|
props[`level${l++}`] = {
|
||||||
component: part.component,
|
component: part.component,
|
||||||
props: Object.assign({}, preloaded[i + 1])
|
props: preloaded[i + 1] || {},
|
||||||
});
|
segment: segments[i]
|
||||||
|
|
||||||
level.props.child = <Props["child"]>{
|
|
||||||
segment: segments[i + 1],
|
|
||||||
props: {}
|
|
||||||
};
|
};
|
||||||
level = level.props.child;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
|
||||||
props.child.props.error = error instanceof Error ? error : { message: error };
|
|
||||||
props.child.props.status = status;
|
|
||||||
}
|
|
||||||
|
|
||||||
stores.page.set({
|
stores.page.set({
|
||||||
path: req.path,
|
path: req.path,
|
||||||
query: req.query,
|
query: req.query,
|
||||||
params: params
|
params: params
|
||||||
});
|
});
|
||||||
|
|
||||||
const { html, head, css } = Sapper.render({
|
const { html, head, css } = App.render(props);
|
||||||
Root: manifest.root,
|
|
||||||
props: props,
|
|
||||||
session: writable(session)
|
|
||||||
});
|
|
||||||
|
|
||||||
const serialized = {
|
const serialized = {
|
||||||
preloaded: `[${preloaded.map(data => try_serialize(data)).join(',')}]`,
|
preloaded: `[${preloaded.map(data => try_serialize(data)).join(',')}]`,
|
||||||
session: session && try_serialize(session, err => {
|
session: session && try_serialize(session, err => {
|
||||||
throw new Error(`Failed to serialize session data: ${err.message}`);
|
throw new Error(`Failed to serialize session data: ${err.message}`);
|
||||||
})
|
}),
|
||||||
|
error: error && try_serialize(props.error)
|
||||||
};
|
};
|
||||||
|
|
||||||
let script = `__SAPPER__={${[
|
let script = `__SAPPER__={${[
|
||||||
error && `error:1`,
|
error && `error:${serialized.error},status:${status}`,
|
||||||
`baseUrl:"${req.baseUrl}"`,
|
`baseUrl:"${req.baseUrl}"`,
|
||||||
serialized.preloaded && `preloaded:${serialized.preloaded}`,
|
serialized.preloaded && `preloaded:${serialized.preloaded}`,
|
||||||
serialized.session && `session:${serialized.session}`
|
serialized.session && `session:${serialized.session}`
|
||||||
@@ -326,12 +330,10 @@ export function get_page_handler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!server_routes.some(route => route.pattern.test(req.path))) {
|
for (const page of pages) {
|
||||||
for (const page of pages) {
|
if (page.pattern.test(req.path)) {
|
||||||
if (page.pattern.test(req.path)) {
|
handle_page(page, req, res);
|
||||||
handle_page(page, req, res);
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,11 +52,6 @@ export default function middleware(opts: {
|
|||||||
next();
|
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({
|
fs.existsSync(path.join(build_dir, 'service-worker.js')) && serve({
|
||||||
pathname: '/service-worker.js',
|
pathname: '/service-worker.js',
|
||||||
cache_control: 'no-cache, no-store, must-revalidate'
|
cache_control: 'no-cache, no-store, must-revalidate'
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import mkdirp from 'mkdirp';
|
|
||||||
import rimraf from 'rimraf';
|
|
||||||
import minify_html from './utils/minify_html';
|
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 { copy_shimport } from './utils/copy_shimport';
|
||||||
import read_template from '../core/read_template';
|
import read_template from '../core/read_template';
|
||||||
import { CompileResult } from '../core/create_compilers/interfaces';
|
import { CompileResult } from '../core/create_compilers/interfaces';
|
||||||
import { noop } from './utils/noop';
|
import { noop } from './utils/noop';
|
||||||
import validate_bundler from './utils/validate_bundler';
|
import validate_bundler from './utils/validate_bundler';
|
||||||
import { copy_runtime } from './utils/copy_runtime';
|
import { copy_runtime } from './utils/copy_runtime';
|
||||||
|
import { rimraf, mkdirp } from './utils/fs_utils';
|
||||||
|
|
||||||
type Opts = {
|
type Opts = {
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
@@ -48,12 +47,12 @@ export async function build({
|
|||||||
throw new Error(`Legacy builds are not supported for projects using webpack`);
|
throw new Error(`Legacy builds are not supported for projects using webpack`);
|
||||||
}
|
}
|
||||||
|
|
||||||
rimraf.sync(path.join(output, '**/*'));
|
rimraf(output);
|
||||||
mkdirp.sync(output);
|
mkdirp(output);
|
||||||
copy_runtime(output);
|
copy_runtime(output);
|
||||||
|
|
||||||
rimraf.sync(path.join(dest, '**/*'));
|
rimraf(dest);
|
||||||
mkdirp.sync(`${dest}/client`);
|
mkdirp(`${dest}/client`);
|
||||||
copy_shimport(dest);
|
copy_shimport(dest);
|
||||||
|
|
||||||
// minify src/template.html
|
// minify src/template.html
|
||||||
@@ -72,7 +71,7 @@ export async function build({
|
|||||||
const manifest_data = create_manifest_data(routes);
|
const manifest_data = create_manifest_data(routes);
|
||||||
|
|
||||||
// create src/node_modules/@sapper/app.mjs and server.mjs
|
// create src/node_modules/@sapper/app.mjs and server.mjs
|
||||||
create_main_manifests({
|
create_app({
|
||||||
bundler,
|
bundler,
|
||||||
manifest_data,
|
manifest_data,
|
||||||
cwd,
|
cwd,
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ import * as fs from 'fs';
|
|||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import * as child_process from 'child_process';
|
import * as child_process from 'child_process';
|
||||||
import * as ports from 'port-authority';
|
import * as ports from 'port-authority';
|
||||||
import mkdirp from 'mkdirp';
|
|
||||||
import rimraf from 'rimraf';
|
|
||||||
import { EventEmitter } from 'events';
|
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 { Compiler, Compilers } from '../core/create_compilers';
|
||||||
import { CompileResult } from '../core/create_compilers/interfaces';
|
import { CompileResult } from '../core/create_compilers/interfaces';
|
||||||
import Deferred from './utils/Deferred';
|
import Deferred from './utils/Deferred';
|
||||||
@@ -16,6 +14,7 @@ import { ManifestData, FatalEvent, ErrorEvent, ReadyEvent, InvalidEvent } from '
|
|||||||
import read_template from '../core/read_template';
|
import read_template from '../core/read_template';
|
||||||
import { noop } from './utils/noop';
|
import { noop } from './utils/noop';
|
||||||
import { copy_runtime } from './utils/copy_runtime';
|
import { copy_runtime } from './utils/copy_runtime';
|
||||||
|
import { rimraf, mkdirp } from './utils/fs_utils';
|
||||||
|
|
||||||
type Opts = {
|
type Opts = {
|
||||||
cwd?: string,
|
cwd?: string,
|
||||||
@@ -146,12 +145,12 @@ class Watcher extends EventEmitter {
|
|||||||
|
|
||||||
const { cwd, src, dest, routes, output, static: static_files } = this.dirs;
|
const { cwd, src, dest, routes, output, static: static_files } = this.dirs;
|
||||||
|
|
||||||
rimraf.sync(path.join(output, '**/*'));
|
rimraf(output);
|
||||||
mkdirp.sync(output);
|
mkdirp(output);
|
||||||
copy_runtime(output);
|
copy_runtime(output);
|
||||||
|
|
||||||
rimraf.sync(dest);
|
rimraf(dest);
|
||||||
mkdirp.sync(`${dest}/client`);
|
mkdirp(`${dest}/client`);
|
||||||
if (this.bundler === 'rollup') copy_shimport(dest);
|
if (this.bundler === 'rollup') copy_shimport(dest);
|
||||||
|
|
||||||
if (!this.dev_port) this.dev_port = await ports.find(10000);
|
if (!this.dev_port) this.dev_port = await ports.find(10000);
|
||||||
@@ -163,7 +162,7 @@ class Watcher extends EventEmitter {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
manifest_data = create_manifest_data(routes);
|
manifest_data = create_manifest_data(routes);
|
||||||
create_main_manifests({
|
create_app({
|
||||||
bundler: this.bundler,
|
bundler: this.bundler,
|
||||||
manifest_data,
|
manifest_data,
|
||||||
dev: true,
|
dev: true,
|
||||||
@@ -191,7 +190,7 @@ class Watcher extends EventEmitter {
|
|||||||
() => {
|
() => {
|
||||||
try {
|
try {
|
||||||
const new_manifest_data = create_manifest_data(routes);
|
const new_manifest_data = create_manifest_data(routes);
|
||||||
create_main_manifests({
|
create_app({
|
||||||
bundler: this.bundler,
|
bundler: this.bundler,
|
||||||
manifest_data, // TODO is this right? not new_manifest_data?
|
manifest_data, // TODO is this right? not new_manifest_data?
|
||||||
dev: true,
|
dev: true,
|
||||||
@@ -200,9 +199,10 @@ class Watcher extends EventEmitter {
|
|||||||
});
|
});
|
||||||
|
|
||||||
manifest_data = new_manifest_data;
|
manifest_data = new_manifest_data;
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
this.emit('error', <ErrorEvent>{
|
this.emit('error', <ErrorEvent>{
|
||||||
message: err.message
|
type: 'manifest',
|
||||||
|
error
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -409,11 +409,11 @@ class Watcher extends EventEmitter {
|
|||||||
}) {
|
}) {
|
||||||
compiler.oninvalid(invalid);
|
compiler.oninvalid(invalid);
|
||||||
|
|
||||||
compiler.watch((err?: Error, result?: CompileResult) => {
|
compiler.watch((error?: Error, result?: CompileResult) => {
|
||||||
if (err) {
|
if (error) {
|
||||||
this.emit('error', <ErrorEvent>{
|
this.emit('error', <ErrorEvent>{
|
||||||
type: name,
|
type: name,
|
||||||
message: err.message
|
error
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.emit('build', {
|
this.emit('build', {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as child_process from 'child_process';
|
import * as child_process from 'child_process';
|
||||||
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as sander from 'sander';
|
|
||||||
import * as url from 'url';
|
import * as url from 'url';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
import * as yootils from 'yootils';
|
import * as yootils from 'yootils';
|
||||||
@@ -9,6 +9,8 @@ import clean_html from './utils/clean_html';
|
|||||||
import minify_html from './utils/minify_html';
|
import minify_html from './utils/minify_html';
|
||||||
import Deferred from './utils/Deferred';
|
import Deferred from './utils/Deferred';
|
||||||
import { noop } from './utils/noop';
|
import { noop } from './utils/noop';
|
||||||
|
import { parse as parseLinkHeader } from 'http-link-header';
|
||||||
|
import { rimraf, copy, mkdirp } from './utils/fs_utils';
|
||||||
|
|
||||||
type Opts = {
|
type Opts = {
|
||||||
build_dir?: string,
|
build_dir?: string,
|
||||||
@@ -21,6 +23,12 @@ type Opts = {
|
|||||||
onfile?: ({ file, size, status }: { file: string, size: number, status: number }) => void;
|
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) {
|
function resolve(from: string, to: string) {
|
||||||
return url.parse(url.resolve(from, to));
|
return url.parse(url.resolve(from, to));
|
||||||
}
|
}
|
||||||
@@ -47,20 +55,15 @@ async function _export({
|
|||||||
export_dir = path.resolve(cwd, export_dir, basepath);
|
export_dir = path.resolve(cwd, export_dir, basepath);
|
||||||
|
|
||||||
// Prep output directory
|
// Prep output directory
|
||||||
sander.rimrafSync(export_dir);
|
rimraf(export_dir);
|
||||||
|
|
||||||
sander.copydirSync(static_files).to(export_dir);
|
copy(static_files, export_dir);
|
||||||
sander.copydirSync(build_dir, 'client').to(export_dir, 'client');
|
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')) {
|
const defaultPort = process.env.PORT ? parseInt(process.env.PORT) : 3000;
|
||||||
sander.copyFileSync(build_dir, 'service-worker.js').to(export_dir, 'service-worker.js');
|
const port = await ports.find(defaultPort);
|
||||||
}
|
|
||||||
|
|
||||||
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 protocol = 'http:';
|
const protocol = 'http:';
|
||||||
const host = `localhost:${port}`;
|
const host = `localhost:${port}`;
|
||||||
@@ -85,8 +88,8 @@ async function _export({
|
|||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
const saved = new Set();
|
const saved = new Set();
|
||||||
|
|
||||||
function save(path: string, status: number, type: string, body: string) {
|
function save(url: string, status: number, type: string, body: string) {
|
||||||
const { pathname } = resolve(origin, path);
|
const { pathname } = resolve(origin, url);
|
||||||
let file = decodeURIComponent(pathname.slice(1));
|
let file = decodeURIComponent(pathname.slice(1));
|
||||||
|
|
||||||
if (saved.has(file)) return;
|
if (saved.has(file)) return;
|
||||||
@@ -107,7 +110,9 @@ async function _export({
|
|||||||
status
|
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 => {
|
proc.on('message', message => {
|
||||||
@@ -139,38 +144,48 @@ async function _export({
|
|||||||
clearTimeout(the_timeout); // prevent it hanging at the end
|
clearTimeout(the_timeout); // prevent it hanging at the end
|
||||||
|
|
||||||
let type = r.headers.get('Content-Type');
|
let type = r.headers.get('Content-Type');
|
||||||
|
|
||||||
let body = await r.text();
|
let body = await r.text();
|
||||||
|
|
||||||
const range = ~~(r.status / 100);
|
const range = ~~(r.status / 100);
|
||||||
|
|
||||||
if (range === 2) {
|
if (range === 2) {
|
||||||
if (type === 'text/html' && pathname !== '/service-worker-index.html') {
|
if (type === 'text/html') {
|
||||||
const cleaned = clean_html(body);
|
// 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);
|
const q = yootils.queue(8);
|
||||||
let promise;
|
|
||||||
|
|
||||||
const base_match = /<base ([\s\S]+?)>/m.exec(cleaned);
|
const base_match = /<base ([\s\S]+?)>/m.exec(cleaned);
|
||||||
const base_href = base_match && get_href(base_match[1]);
|
const base_href = base_match && get_href(base_match[1]);
|
||||||
const base = resolve(url.href, base_href);
|
const base = resolve(url.href, base_href);
|
||||||
|
|
||||||
let match;
|
let match;
|
||||||
let pattern = /<a ([\s\S]+?)>/gm;
|
let pattern = /<a ([\s\S]+?)>/gm;
|
||||||
|
|
||||||
while (match = pattern.exec(cleaned)) {
|
while (match = pattern.exec(cleaned)) {
|
||||||
const attrs = match[1];
|
const attrs = match[1];
|
||||||
const href = get_href(attrs);
|
const href = get_href(attrs);
|
||||||
|
|
||||||
if (href) {
|
if (href) {
|
||||||
const url = resolve(base.href, href);
|
const url = resolve(base.href, href);
|
||||||
|
|
||||||
if (url.protocol === protocol && url.host === host) {
|
if (url.protocol === protocol && url.host === host) {
|
||||||
promise = q.add(() => handle(url));
|
q.add(() => handle(url));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
await promise;
|
await q.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,6 +212,6 @@ async function _export({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function get_href(attrs: string) {
|
function get_href(attrs: string) {
|
||||||
const match = /href\s*=\s*(?:"(.*?)"|'(.+?)'|([^\s>]+))/.exec(attrs);
|
const match = /href\s*=\s*(?:"(.*?)"|'(.*?)'|([^\s>]*))/.exec(attrs);
|
||||||
return match[1] || match[2] || match[3];
|
return match && (match[1] || match[2] || match[3]);
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import mkdirp from 'mkdirp';
|
import { mkdirp } from './fs_utils';
|
||||||
|
|
||||||
const runtime = [
|
const runtime = [
|
||||||
'app.mjs',
|
'app.mjs',
|
||||||
'server.mjs',
|
'server.mjs',
|
||||||
'internal/shared.mjs',
|
'internal/shared.mjs',
|
||||||
'internal/Sapper.html',
|
'internal/layout.svelte',
|
||||||
'internal/layout.html'
|
'internal/error.svelte'
|
||||||
].map(file => ({
|
].map(file => ({
|
||||||
file,
|
file,
|
||||||
source: fs.readFileSync(path.join(__dirname, `../runtime/${file}`), 'utf-8')
|
source: fs.readFileSync(path.join(__dirname, `../runtime/${file}`), 'utf-8')
|
||||||
@@ -15,7 +15,7 @@ const runtime = [
|
|||||||
|
|
||||||
export function copy_runtime(output: string) {
|
export function copy_runtime(output: string) {
|
||||||
runtime.forEach(({ file, source }) => {
|
runtime.forEach(({ file, source }) => {
|
||||||
mkdirp.sync(path.dirname(`${output}/${file}`));
|
mkdirp(path.dirname(`${output}/${file}`));
|
||||||
fs.writeFileSync(`${output}/${file}`, source);
|
fs.writeFileSync(`${output}/${file}`, source);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
46
src/api/utils/fs_utils.ts
Normal file
46
src/api/utils/fs_utils.ts
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/cli.ts
20
src/cli.ts
@@ -89,8 +89,16 @@ prog.command('dev')
|
|||||||
});
|
});
|
||||||
|
|
||||||
watcher.on('error', (event: ErrorEvent) => {
|
watcher.on('error', (event: ErrorEvent) => {
|
||||||
console.log(colors.red(`✗ ${event.type}`));
|
const { type, error } = event;
|
||||||
console.log(colors.red(event.message));
|
|
||||||
|
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) => {
|
watcher.on('fatal', (event: FatalEvent) => {
|
||||||
@@ -103,7 +111,7 @@ prog.command('dev')
|
|||||||
console.log(colors.bold().red(`✗ ${event.type}`));
|
console.log(colors.bold().red(`✗ ${event.type}`));
|
||||||
|
|
||||||
event.errors.filter(e => !e.duplicate).forEach(error => {
|
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);
|
console.log(error.message);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -115,7 +123,7 @@ prog.command('dev')
|
|||||||
console.log(colors.bold().yellow(`• ${event.type}`));
|
console.log(colors.bold().yellow(`• ${event.type}`));
|
||||||
|
|
||||||
event.warnings.filter(e => !e.duplicate).forEach(warning => {
|
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);
|
console.log(warning.message);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -268,12 +276,12 @@ async function _build(
|
|||||||
|
|
||||||
oncompile: event => {
|
oncompile: event => {
|
||||||
let banner = `built ${event.type}`;
|
let banner = `built ${event.type}`;
|
||||||
let c = colors.cyan;
|
let c = (txt: string) => colors.cyan(txt);
|
||||||
|
|
||||||
const { warnings } = event.result;
|
const { warnings } = event.result;
|
||||||
if (warnings.length > 0) {
|
if (warnings.length > 0) {
|
||||||
banner += ` with ${warnings.length} ${warnings.length === 1 ? 'warning' : 'warnings'}`;
|
banner += ` with ${warnings.length} ${warnings.length === 1 ? 'warning' : 'warnings'}`;
|
||||||
c = colors.yellow;
|
c = (txt: string) => colors.cyan(txt);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log();
|
console.log();
|
||||||
|
|||||||
@@ -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_compilers } from './core/create_compilers/index';
|
||||||
export { default as create_manifest_data } from './core/create_manifest_data';
|
export { default as create_manifest_data } from './core/create_manifest_data';
|
||||||
@@ -3,7 +3,7 @@ import * as path from 'path';
|
|||||||
import { posixify, stringify, walk, write_if_changed } from '../utils';
|
import { posixify, stringify, walk, write_if_changed } from '../utils';
|
||||||
import { Page, PageComponent, ManifestData } from '../interfaces';
|
import { Page, PageComponent, ManifestData } from '../interfaces';
|
||||||
|
|
||||||
export function create_main_manifests({
|
export function create_app({
|
||||||
bundler,
|
bundler,
|
||||||
manifest_data,
|
manifest_data,
|
||||||
dev_port,
|
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 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 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-client.mjs`, client_manifest);
|
||||||
write_if_changed(`${output}/internal/manifest-server.mjs`, server_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 }: {
|
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[];
|
client_files: string[];
|
||||||
static_files: string;
|
static_files: string;
|
||||||
}) {
|
}) {
|
||||||
let files: string[] = ['/service-worker-index.html'];
|
let files: string[] = ['service-worker-index.html'];
|
||||||
|
|
||||||
if (fs.existsSync(static_files)) {
|
if (fs.existsSync(static_files)) {
|
||||||
files = files.concat(walk(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!
|
// 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 { 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 { 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(', ')}];
|
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) =>
|
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 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 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> = {};
|
const component_lookup: Record<string, number> = {};
|
||||||
@@ -228,15 +231,61 @@ function generate_server_manifest(
|
|||||||
|
|
||||||
export const dev = ${dev ? 'true' : 'false'};
|
export const dev = ${dev ? 'true' : 'false'};
|
||||||
`.replace(/^\t{2}/gm, '').trim();
|
`.replace(/^\t{2}/gm, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
return `// This file is generated by Sapper — do not edit it!\n` + template
|
function generate_app(manifest_data: ManifestData, path_to_routes: string) {
|
||||||
.replace('__BUILD__DIR__', JSON.stringify(build_dir))
|
// TODO remove default layout altogether
|
||||||
.replace('__SRC__DIR__', JSON.stringify(src_dir))
|
|
||||||
.replace('__DEV__', dev ? 'true' : 'false')
|
const max_depth = Math.max(...manifest_data.pages.map(page => page.parts.filter(Boolean).length));
|
||||||
.replace(/const manifest = __MANIFEST__;/, code);
|
|
||||||
|
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) {
|
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}`);
|
return posixify(`${path_to_routes}/${component.file}`);
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import format_messages from 'webpack-format-messages';
|
import format_messages from 'webpack-format-messages';
|
||||||
import { CompileResult, BuildInfo, CompileError, Chunk, CssFile } from './interfaces';
|
import { CompileResult, BuildInfo, CompileError, Chunk, CssFile } from './interfaces';
|
||||||
import { ManifestData, Dirs } from '../../interfaces';
|
import { ManifestData, Dirs, PageComponent } from '../../interfaces';
|
||||||
|
|
||||||
const locPattern = /\((\d+):(\d+)\)$/;
|
const locPattern = /\((\d+):(\d+)\)$/;
|
||||||
|
|
||||||
@@ -66,12 +66,15 @@ export default class WebpackResult implements CompileResult {
|
|||||||
assets: this.assets,
|
assets: this.assets,
|
||||||
css: {
|
css: {
|
||||||
main: extract_css(this.assets.main),
|
main: extract_css(this.assets.main),
|
||||||
chunks: Object
|
chunks: manifest_data.components
|
||||||
.keys(this.assets)
|
.reduce((chunks: Record<string, string[]>, component: PageComponent) => {
|
||||||
.filter(chunkName => chunkName !== 'main')
|
const css_dependencies = [];
|
||||||
.reduce((chunks: { [key: string]: string }, chukName) => {
|
const css = extract_css(this.assets[component.name]);
|
||||||
const assets = this.assets[chukName];
|
|
||||||
chunks[chukName] = extract_css(assets);
|
if (css) css_dependencies.push(css);
|
||||||
|
|
||||||
|
chunks[component.file] = css_dependencies;
|
||||||
|
|
||||||
return chunks;
|
return chunks;
|
||||||
}, {})
|
}, {})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ export default function extract_css(client_result: CompileResult, components: Pa
|
|||||||
chunks_with_css.add(chunk);
|
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 = client_result.chunks.find(chunk => chunk.modules.indexOf(entry) !== -1);
|
||||||
|
|
||||||
const entry_chunk_dependencies: Set<Chunk> = new Set([entry_chunk]);
|
const entry_chunk_dependencies: Set<Chunk> = new Set([entry_chunk]);
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import svelte from 'svelte/compiler';
|
|||||||
import { Page, PageComponent, ServerRoute, ManifestData } from '../interfaces';
|
import { Page, PageComponent, ServerRoute, ManifestData } from '../interfaces';
|
||||||
import { posixify, reserved_words } from '../utils';
|
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 {
|
export default function create_manifest_data(cwd: string): ManifestData {
|
||||||
// TODO remove in a future version
|
// TODO remove in a future version
|
||||||
if (!fs.existsSync(cwd)) {
|
if (!fs.existsSync(cwd)) {
|
||||||
@@ -15,11 +17,8 @@ export default function create_manifest_data(cwd: string): ManifestData {
|
|||||||
|
|
||||||
if (/preload/.test(source)) {
|
if (/preload/.test(source)) {
|
||||||
try {
|
try {
|
||||||
const { stats } = svelte.compile(source, {
|
const { vars } = svelte.compile(source.replace(/<style\b[^>]*>[^]*?<\/style>/g, ''), { generate: false });
|
||||||
generate: false,
|
return vars.some((variable: any) => variable.module && variable.export_name === 'preload');
|
||||||
onwarn: () => {}
|
|
||||||
});
|
|
||||||
return !!stats.vars.find((variable: any) => variable.module && variable.export_name === 'preload');
|
|
||||||
} catch (err) {}
|
} catch (err) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,11 +31,20 @@ export default function create_manifest_data(cwd: string): ManifestData {
|
|||||||
|
|
||||||
const default_layout: PageComponent = {
|
const default_layout: PageComponent = {
|
||||||
default: true,
|
default: true,
|
||||||
|
type: 'layout',
|
||||||
name: '_default_layout',
|
name: '_default_layout',
|
||||||
file: null,
|
file: null,
|
||||||
has_preload: false
|
has_preload: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const default_error: PageComponent = {
|
||||||
|
default: true,
|
||||||
|
type: 'error',
|
||||||
|
name: '_default_error',
|
||||||
|
file: null,
|
||||||
|
has_preload: false
|
||||||
|
};
|
||||||
|
|
||||||
function walk(
|
function walk(
|
||||||
dir: string,
|
dir: string,
|
||||||
parent_segments: Part[][],
|
parent_segments: Part[][],
|
||||||
@@ -61,7 +69,7 @@ export default function create_manifest_data(cwd: string): ManifestData {
|
|||||||
|
|
||||||
const parts = get_parts(segment);
|
const parts = get_parts(segment);
|
||||||
const is_index = is_dir ? false : basename.startsWith('index.');
|
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 => {
|
parts.forEach(part => {
|
||||||
if (/\]\[/.test(part.content)) {
|
if (/\]\[/.test(part.content)) {
|
||||||
@@ -75,6 +83,7 @@ export default function create_manifest_data(cwd: string): ManifestData {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
basename,
|
basename,
|
||||||
|
ext,
|
||||||
parts,
|
parts,
|
||||||
file: posixify(file),
|
file: posixify(file),
|
||||||
is_dir,
|
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));
|
params.push(...item.parts.filter(p => p.dynamic).map(p => p.content));
|
||||||
|
|
||||||
if (item.is_dir) {
|
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`,
|
name: `${get_slug(item.file)}__layout`,
|
||||||
file: `${item.file}/_layout.html`,
|
file: `${item.file}/_layout${ext}`,
|
||||||
has_preload: has_preload(`${item.file}/_layout.html`)
|
has_preload: has_preload(`${item.file}/_layout${ext}`)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (component) components.push(component);
|
if (component) components.push(component);
|
||||||
@@ -142,29 +154,26 @@ export default function create_manifest_data(cwd: string): ManifestData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
else if (item.is_page) {
|
else if (item.is_page) {
|
||||||
|
const is_index = item.basename === `index${item.ext}`;
|
||||||
|
|
||||||
const component = {
|
const component = {
|
||||||
name: get_slug(item.file),
|
name: get_slug(item.file),
|
||||||
file: item.file,
|
file: item.file,
|
||||||
has_preload: has_preload(item.file)
|
has_preload: has_preload(item.file)
|
||||||
};
|
};
|
||||||
|
|
||||||
const parts = stack.concat({
|
|
||||||
component,
|
|
||||||
params
|
|
||||||
});
|
|
||||||
|
|
||||||
components.push(component);
|
components.push(component);
|
||||||
if (item.basename === 'index.html') {
|
|
||||||
pages.push({
|
const parts = (is_index && stack[stack.length - 1] === null)
|
||||||
pattern: get_pattern(parent_segments, true),
|
? stack.slice(0, -1).concat({ component, params })
|
||||||
parts
|
: stack.concat({ component, params })
|
||||||
});
|
|
||||||
} else {
|
const page = {
|
||||||
pages.push({
|
pattern: get_pattern(is_index ? parent_segments : segments, true),
|
||||||
pattern: get_pattern(segments, true),
|
parts
|
||||||
parts
|
};
|
||||||
});
|
|
||||||
}
|
pages.push(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
else {
|
else {
|
||||||
@@ -178,15 +187,24 @@ export default function create_manifest_data(cwd: string): ManifestData {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const root_file = path.join(cwd, '_layout.html');
|
const root_ext = component_extensions.find(ext => fs.existsSync(path.join(cwd, `_layout${ext}`)));
|
||||||
const root = fs.existsSync(root_file)
|
const root = root_ext
|
||||||
? {
|
? {
|
||||||
name: 'main',
|
name: 'main',
|
||||||
file: '_layout.html',
|
file: `_layout${root_ext}`,
|
||||||
has_preload: has_preload('_layout.html')
|
has_preload: has_preload(`_layout${root_ext}`)
|
||||||
}
|
}
|
||||||
: default_layout;
|
: 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, [], [], []);
|
walk(cwd, [], [], []);
|
||||||
|
|
||||||
// check for clashes
|
// check for clashes
|
||||||
@@ -217,6 +235,7 @@ export default function create_manifest_data(cwd: string): ManifestData {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
root,
|
root,
|
||||||
|
error,
|
||||||
components,
|
components,
|
||||||
pages,
|
pages,
|
||||||
server_routes
|
server_routes
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export type WritableStore<T> = {
|
|||||||
|
|
||||||
export type PageComponent = {
|
export type PageComponent = {
|
||||||
default?: boolean;
|
default?: boolean;
|
||||||
|
type?: string;
|
||||||
name: string;
|
name: string;
|
||||||
file: string;
|
file: string;
|
||||||
has_preload: boolean;
|
has_preload: boolean;
|
||||||
@@ -55,6 +56,7 @@ export type Dirs = {
|
|||||||
|
|
||||||
export type ManifestData = {
|
export type ManifestData = {
|
||||||
root: PageComponent;
|
root: PageComponent;
|
||||||
|
error: PageComponent;
|
||||||
components: PageComponent[];
|
components: PageComponent[];
|
||||||
pages: Page[];
|
pages: Page[];
|
||||||
server_routes: ServerRoute[];
|
server_routes: ServerRoute[];
|
||||||
@@ -67,7 +69,7 @@ export type ReadyEvent = {
|
|||||||
|
|
||||||
export type ErrorEvent = {
|
export type ErrorEvent = {
|
||||||
type: string;
|
type: string;
|
||||||
message: string;
|
error: Error;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FatalEvent = {
|
export type FatalEvent = {
|
||||||
|
|||||||
3
test/apps/basics/src/routes/dirs/bar/[a].svelte
Normal file
3
test/apps/basics/src/routes/dirs/bar/[a].svelte
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<h1>A page</h1>
|
||||||
|
|
||||||
|
<a href="dirs/foo/xyz">same segment</a>
|
||||||
1
test/apps/basics/src/routes/dirs/bar/index.svelte
Normal file
1
test/apps/basics/src/routes/dirs/bar/index.svelte
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<h1>bar</h1>
|
||||||
1
test/apps/basics/src/routes/dirs/foo/[b].svelte
Normal file
1
test/apps/basics/src/routes/dirs/foo/[b].svelte
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<h1>B page</h1>
|
||||||
2
test/apps/basics/src/routes/dirs/foo/index.svelte
Normal file
2
test/apps/basics/src/routes/dirs/foo/index.svelte
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<h1>foo</h1>
|
||||||
|
<a href="dirs/bar">bar</a>
|
||||||
8
test/apps/basics/src/routes/middleware/index.js
Normal file
8
test/apps/basics/src/routes/middleware/index.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export function get(req, res, next) {
|
||||||
|
if (req.headers.accept === 'application/json') {
|
||||||
|
res.end('{"json":true}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
1
test/apps/basics/src/routes/middleware/index.svelte
Normal file
1
test/apps/basics/src/routes/middleware/index.svelte
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<h1>HTML</h1>
|
||||||
@@ -8,6 +8,25 @@ import { wait } from '../../utils';
|
|||||||
declare let deleted: { id: number };
|
declare let deleted: { id: number };
|
||||||
declare let el: any;
|
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() {
|
describe('basics', function() {
|
||||||
this.timeout(10000);
|
this.timeout(10000);
|
||||||
|
|
||||||
@@ -116,38 +135,25 @@ describe('basics', function() {
|
|||||||
assert.equal(requests[1], `${base}/b.json`);
|
assert.equal(requests[1], `${base}/b.json`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// TODO equivalent test for a webpack app
|
// TODO equivalent test for a webpack app
|
||||||
it('sets Content-Type, Link...modulepreload, and Cache-Control headers', () => {
|
it('sets Content-Type, Link...modulepreload, and Cache-Control headers', async () => {
|
||||||
return new Promise((fulfil, reject) => {
|
const { headers } = await get(base);
|
||||||
const req = http.get(base, res => {
|
|
||||||
try {
|
|
||||||
const { headers } = res;
|
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
headers['content-type'],
|
headers['content-type'],
|
||||||
'text/html'
|
'text/html'
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
headers['cache-control'],
|
headers['cache-control'],
|
||||||
'max-age=600'
|
'max-age=600'
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO preload more than just the entry point
|
// TODO preload more than just the entry point
|
||||||
const regex = /<\/client\/client\.\w+\.js>;rel="modulepreload"/;
|
const regex = /<\/client\/client\.\w+\.js>;rel="modulepreload"/;
|
||||||
const link = <string>headers['link'];
|
const link = <string>headers['link'];
|
||||||
|
|
||||||
assert.ok(regex.test(link), link);
|
assert.ok(regex.test(link), link);
|
||||||
|
|
||||||
fulfil();
|
|
||||||
} catch (err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
req.on('error', reject);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls a delete handler', async () => {
|
it('calls a delete handler', async () => {
|
||||||
@@ -264,4 +270,40 @@ describe('basics', function() {
|
|||||||
const html = String(await page.evaluate(() => document.body.innerHTML));
|
const html = String(await page.evaluate(() => document.body.innerHTML));
|
||||||
assert.equal(html.indexOf('%sapper'), -1);
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
export let Title;
|
export let Title;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
import('./_components/Title.html').then(mod => {
|
import('./_components/Title.svelte').then(mod => {
|
||||||
Title = mod.default;
|
Title = mod.default;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
17
test/apps/errors/src/routes/_error.svelte
Normal file
17
test/apps/errors/src/routes/_error.svelte
Normal 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>
|
||||||
@@ -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 () => {
|
it('does not serve error page for async non-page error', async () => {
|
||||||
await page.goto(`${base}/async-throw.json`);
|
await page.goto(`${base}/async-throw.json`);
|
||||||
|
|
||||||
|
|||||||
9
test/apps/export-webpack/src/client.js
Normal file
9
test/apps/export-webpack/src/client.js
Normal 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);
|
||||||
13
test/apps/export-webpack/src/routes/blog/[slug].html
Normal file
13
test/apps/export-webpack/src/routes/blog/[slug].html
Normal 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>
|
||||||
19
test/apps/export-webpack/src/routes/blog/[slug].json.js
Normal file
19
test/apps/export-webpack/src/routes/blog/[slug].json.js
Normal 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' }));
|
||||||
|
}
|
||||||
|
}
|
||||||
5
test/apps/export-webpack/src/routes/blog/_posts.js
Normal file
5
test/apps/export-webpack/src/routes/blog/_posts.js
Normal 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' }
|
||||||
|
];
|
||||||
17
test/apps/export-webpack/src/routes/blog/index.html
Normal file
17
test/apps/export-webpack/src/routes/blog/index.html
Normal 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}
|
||||||
9
test/apps/export-webpack/src/routes/blog/index.json.js
Normal file
9
test/apps/export-webpack/src/routes/blog/index.json.js
Normal 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));
|
||||||
|
}
|
||||||
15
test/apps/export-webpack/src/server.js
Normal file
15
test/apps/export-webpack/src/server.js
Normal 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);
|
||||||
|
});
|
||||||
82
test/apps/export-webpack/src/service-worker.js
Normal file
82
test/apps/export-webpack/src/service-worker.js
Normal 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;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
14
test/apps/export-webpack/src/template.html
Normal file
14
test/apps/export-webpack/src/template.html
Normal 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>
|
||||||
3
test/apps/export-webpack/static/global.css
Normal file
3
test/apps/export-webpack/static/global.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
body {
|
||||||
|
font-family: 'Comic Sans MS';
|
||||||
|
}
|
||||||
19
test/apps/export-webpack/test.ts
Normal file
19
test/apps/export-webpack/test.ts
Normal 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));
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
73
test/apps/export-webpack/webpack.config.js
Normal file
73
test/apps/export-webpack/webpack.config.js
Normal 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'
|
||||||
|
}
|
||||||
|
};
|
||||||
9
test/apps/export/src/routes/index.svelte
Normal file
9
test/apps/export/src/routes/index.svelte
Normal 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>
|
||||||
@@ -12,10 +12,10 @@
|
|||||||
import { page } from '@sapper/app';
|
import { page } from '@sapper/app';
|
||||||
|
|
||||||
export let count;
|
export let count;
|
||||||
export let child;
|
export let segment;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span>y: {$page.params.y} {count}</span>
|
<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>
|
||||||
@@ -28,7 +28,7 @@ describe('layout', function() {
|
|||||||
await page.goto(`${base}/foo/bar/baz`);
|
await page.goto(`${base}/foo/bar/baz`);
|
||||||
|
|
||||||
const text1 = String(await page.evaluate(() => document.querySelector('#sapper').textContent));
|
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',
|
'y: bar 1',
|
||||||
'z: baz 1',
|
'z: baz 1',
|
||||||
'click me',
|
'click me',
|
||||||
@@ -37,7 +37,7 @@ describe('layout', function() {
|
|||||||
|
|
||||||
await start();
|
await start();
|
||||||
const text2 = String(await page.evaluate(() => document.querySelector('#sapper').textContent));
|
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',
|
'y: bar 1',
|
||||||
'z: baz 1',
|
'z: baz 1',
|
||||||
'click me',
|
'click me',
|
||||||
@@ -48,7 +48,7 @@ describe('layout', function() {
|
|||||||
await wait(50);
|
await wait(50);
|
||||||
|
|
||||||
const text3 = String(await page.evaluate(() => document.querySelector('#sapper').textContent));
|
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',
|
'y: bar 1',
|
||||||
'z: qux 2',
|
'z: qux 2',
|
||||||
'click me',
|
'click me',
|
||||||
|
|||||||
@@ -8,13 +8,16 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { preloading } from '@sapper/app';
|
import { preloading } from '@sapper/app';
|
||||||
|
import { setContext } from 'svelte';
|
||||||
|
|
||||||
export let child;
|
export let child;
|
||||||
export let rootPreloadFunctionRan;
|
export let rootPreloadFunctionRan;
|
||||||
|
|
||||||
|
setContext('x', { rootPreloadFunctionRan });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $preloading}
|
{#if $preloading}
|
||||||
<progress class='preloading-progress' value=0.5/>
|
<progress class='preloading-progress' value=0.5/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<svelte:component this={child.component} {rootPreloadFunctionRan} {...child.props}/>
|
<slot></slot>
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svelte:component this={child.component} {...child.props}/>
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<script context="module">
|
||||||
|
export function preload() {}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1>Page loaded</h1>
|
||||||
@@ -1 +0,0 @@
|
|||||||
<h1>root preload function ran: {rootPreloadFunctionRan}</h1>
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<script>
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
const { rootPreloadFunctionRan } = getContext('x');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1>root preload function ran: {rootPreloadFunctionRan}</h1>
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svelte:component this={child.component} {...child.props}/>
|
|
||||||
@@ -37,6 +37,15 @@ describe('preloading', function() {
|
|||||||
assert.equal(await title(), 'true');
|
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 () => {
|
it('bails on custom classes returned from preload', async () => {
|
||||||
await page.goto(`${base}/preload-values/custom-class`);
|
await page.goto(`${base}/preload-values/custom-class`);
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user