mirror of
https://github.com/kevin-DL/sapper.git
synced 2026-01-13 19:45:26 +00:00
Compare commits
116 Commits
| 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 | ||
|
|
7150c7e088 | ||
|
|
a85e1424e3 | ||
|
|
0628ea99ab | ||
|
|
3d7cfbbf3d | ||
|
|
66be631572 | ||
|
|
c964500118 | ||
|
|
91ea0335ae | ||
|
|
11d3da3aed | ||
|
|
7fefc59929 | ||
|
|
3521eff4f4 | ||
|
|
b7fce99438 | ||
|
|
85c86b5562 | ||
|
|
81bbfce448 | ||
|
|
6e8ba295d4 | ||
|
|
0168d8b70c | ||
|
|
548de702ac | ||
|
|
7ba1a0a9fa | ||
|
|
3bab780f88 | ||
|
|
263bb08334 | ||
|
|
3445ec66ac | ||
|
|
4940c5d5be | ||
|
|
83c8d7f855 | ||
|
|
fdfe282130 | ||
|
|
b5fbc7e0e8 | ||
|
|
64eb3f856a | ||
|
|
18d8e61ecb | ||
|
|
2a635f92a9 | ||
|
|
44bcbeb7d6 | ||
|
|
4023831b18 | ||
|
|
f587161d7d | ||
|
|
d486542a8b | ||
|
|
c990c771d8 | ||
|
|
90f3393ebf | ||
|
|
76ce7f227f | ||
|
|
1f9efd353c | ||
|
|
3499631e8e | ||
|
|
2a825269e9 | ||
|
|
83f7102f6b | ||
|
|
e4319bee0e | ||
|
|
da540ef15f | ||
|
|
c00af6dad0 | ||
|
|
92206742d4 | ||
|
|
14fc6b3176 | ||
|
|
b6bc90cea9 | ||
|
|
cfba9b2168 | ||
|
|
b25c642bf1 | ||
|
|
82a023c302 | ||
|
|
8108642845 | ||
|
|
18e6f29de7 | ||
|
|
d0c6b9cdca |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -12,4 +12,5 @@ sapper
|
|||||||
runtime.js
|
runtime.js
|
||||||
dist
|
dist
|
||||||
!rollup.config.js
|
!rollup.config.js
|
||||||
templates/*.js
|
/runtime/app.mjs
|
||||||
|
/runtime/server.mjs
|
||||||
@@ -3,7 +3,6 @@ sudo: false
|
|||||||
language: node_js
|
language: node_js
|
||||||
|
|
||||||
node_js:
|
node_js:
|
||||||
- "6"
|
|
||||||
- "stable"
|
- "stable"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# sapper changelog
|
# sapper changelog
|
||||||
|
|
||||||
|
## 0.25.0
|
||||||
|
|
||||||
|
* Force refresh on `goto(current_url)` ([#484](https://github.com/sveltejs/sapper/pull/484))
|
||||||
|
* Fix preloading navigation bug ([#532](https://github.com/sveltejs/sapper/issues/532))
|
||||||
|
* Don't mutate opts.headers ([#528](https://github.com/sveltejs/sapper/issues/528))
|
||||||
|
* Don't crawl hundreds of pages simultaneously ([#369](https://github.com/sveltejs/sapper/pull/369))
|
||||||
|
|
||||||
## 0.24.3
|
## 0.24.3
|
||||||
|
|
||||||
* Add service-worker-index.html shell file for offline support ([#422](https://github.com/sveltejs/sapper/issues/422))
|
* Add service-worker-index.html shell file for offline support ([#422](https://github.com/sveltejs/sapper/issues/422))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
--require source-map-support/register
|
--require source-map-support/register
|
||||||
--require ts-node/register
|
--require sucrase/register
|
||||||
--recursive
|
--recursive
|
||||||
test/unit/*/*.ts
|
|
||||||
test/apps/*/test.ts
|
test/apps/*/test.ts
|
||||||
3315
package-lock.json
generated
3315
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
66
package.json
66
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sapper",
|
"name": "sapper",
|
||||||
"version": "0.24.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"
|
||||||
@@ -11,59 +11,57 @@
|
|||||||
"config",
|
"config",
|
||||||
"sapper",
|
"sapper",
|
||||||
"dist/*.js",
|
"dist/*.js",
|
||||||
"templates/*.js"
|
"runtime/*.mjs",
|
||||||
|
"runtime/internal"
|
||||||
],
|
],
|
||||||
"directories": {
|
"directories": {
|
||||||
"test": "test"
|
"test": "test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"html-minifier": "^3.5.20",
|
"html-minifier": "^3.5.21",
|
||||||
"shimport": "0.0.11",
|
"http-link-header": "^1.0.2",
|
||||||
"source-map-support": "^0.5.9",
|
"shimport": "0.0.14",
|
||||||
"sourcemap-codec": "^1.4.3",
|
"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.0",
|
"@types/node": "^10.12.21",
|
||||||
"@types/puppeteer": "^1.9.0",
|
"@types/puppeteer": "^1.11.3",
|
||||||
"@types/rimraf": "^2.0.2",
|
|
||||||
"agadoo": "^1.0.1",
|
"agadoo": "^1.0.1",
|
||||||
"cheap-watch": "^1.0.0",
|
"cheap-watch": "^1.0.2",
|
||||||
"cookie": "^0.3.1",
|
"cookie": "^0.3.1",
|
||||||
"devalue": "^1.0.4",
|
"devalue": "^1.1.0",
|
||||||
"eslint": "^5.7.0",
|
"eslint": "^5.12.1",
|
||||||
"eslint-plugin-import": "^2.14.0",
|
"eslint-plugin-import": "^2.16.0",
|
||||||
"kleur": "^2.0.2",
|
"kleur": "^3.0.1",
|
||||||
"mkdirp": "^0.5.1",
|
|
||||||
"mocha": "^5.2.0",
|
"mocha": "^5.2.0",
|
||||||
"node-fetch": "^2.2.0",
|
"node-fetch": "^2.3.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"polka": "^0.5.1",
|
"polka": "^0.5.1",
|
||||||
"port-authority": "^1.0.5",
|
"port-authority": "^1.0.5",
|
||||||
"pretty-bytes": "^5.1.0",
|
"pretty-bytes": "^5.1.0",
|
||||||
"puppeteer": "^1.9.0",
|
"puppeteer": "^1.12.0",
|
||||||
"require-relative": "^0.8.7",
|
"require-relative": "^0.8.7",
|
||||||
"rimraf": "^2.6.2",
|
"rollup": "^1.1.2",
|
||||||
"rollup": "^0.66.6",
|
|
||||||
"rollup-plugin-commonjs": "^9.2.0",
|
"rollup-plugin-commonjs": "^9.2.0",
|
||||||
"rollup-plugin-json": "^3.1.0",
|
"rollup-plugin-json": "^3.1.0",
|
||||||
"rollup-plugin-node-resolve": "^3.4.0",
|
"rollup-plugin-node-resolve": "^4.0.0",
|
||||||
"rollup-plugin-replace": "^2.1.0",
|
"rollup-plugin-replace": "^2.1.0",
|
||||||
"rollup-plugin-string": "^2.0.2",
|
"rollup-plugin-string": "^2.0.2",
|
||||||
"rollup-plugin-svelte": "^4.3.2",
|
"rollup-plugin-sucrase": "^2.1.0",
|
||||||
"rollup-plugin-typescript": "^1.0.0",
|
"rollup-plugin-svelte": "^5.0.3",
|
||||||
"sade": "^1.4.1",
|
"sade": "^1.4.2",
|
||||||
"sander": "^0.6.0",
|
|
||||||
"sirv": "^0.2.2",
|
"sirv": "^0.2.2",
|
||||||
"svelte": "^2.13.5",
|
"sucrase": "^3.9.5",
|
||||||
"svelte-loader": "^2.11.0",
|
"svelte": "^3.0.0-beta.11",
|
||||||
"ts-node": "^7.0.1",
|
"svelte-loader": "^2.13.3",
|
||||||
"typescript": "^3.1.3",
|
"webpack": "^4.29.0",
|
||||||
"webpack": "^4.20.2",
|
"webpack-format-messages": "^2.0.5",
|
||||||
"webpack-format-messages": "^2.0.3"
|
"yootils": "0.0.14"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^3.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "mocha --opts mocha.opts",
|
"test": "mocha --opts mocha.opts",
|
||||||
@@ -72,7 +70,7 @@
|
|||||||
"prepare": "npm run build",
|
"prepare": "npm run build",
|
||||||
"dev": "rollup -cw",
|
"dev": "rollup -cw",
|
||||||
"prepublishOnly": "npm test",
|
"prepublishOnly": "npm test",
|
||||||
"update_mime_types": "curl http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types | grep -e \"^[^#]\" > templates/src/server/middleware/mime-types.md"
|
"update_mime_types": "curl http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types | grep -e \"^[^#]\" > runtime/src/server/middleware/mime-types.md"
|
||||||
},
|
},
|
||||||
"repository": "https://github.com/sveltejs/sapper",
|
"repository": "https://github.com/sveltejs/sapper",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import typescript from 'rollup-plugin-typescript';
|
import sucrase from 'rollup-plugin-sucrase';
|
||||||
import string from 'rollup-plugin-string';
|
import string from 'rollup-plugin-string';
|
||||||
import json from 'rollup-plugin-json';
|
import json from 'rollup-plugin-json';
|
||||||
import resolve from 'rollup-plugin-node-resolve';
|
import resolve from 'rollup-plugin-node-resolve';
|
||||||
@@ -9,34 +9,37 @@ 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, target) {
|
function template(kind, external) {
|
||||||
return {
|
return {
|
||||||
input: `templates/src/${kind}/index.ts`,
|
input: `runtime/src/${kind}/index.ts`,
|
||||||
output: {
|
output: {
|
||||||
file: `templates/${kind}.js`,
|
file: `runtime/${kind}.mjs`,
|
||||||
format: 'es'
|
format: 'es',
|
||||||
|
paths: id => id.replace('@sapper', '.')
|
||||||
},
|
},
|
||||||
external,
|
external,
|
||||||
plugins: [
|
plugins: [
|
||||||
resolve(),
|
resolve({
|
||||||
|
extensions: ['.mjs', '.js', '.ts']
|
||||||
|
}),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
string({
|
string({
|
||||||
include: '**/*.md'
|
include: '**/*.md'
|
||||||
}),
|
}),
|
||||||
typescript({
|
sucrase({
|
||||||
typescript: require('typescript'),
|
transforms: ['typescript']
|
||||||
target
|
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
template('client', ['__ROOT__', '__ERROR__'], 'ES2017'),
|
template('app', id => /^(svelte\/?|@sapper\/)/.test(id)),
|
||||||
template('server', builtinModules, 'ES2015'),
|
template('server', id => /^(svelte\/?|@sapper\/)/.test(id) || builtinModules.includes(id)),
|
||||||
|
|
||||||
{
|
{
|
||||||
input: [
|
input: [
|
||||||
@@ -54,12 +57,13 @@ export default [
|
|||||||
external,
|
external,
|
||||||
plugins: [
|
plugins: [
|
||||||
json(),
|
json(),
|
||||||
resolve(),
|
resolve({
|
||||||
|
extensions: ['.mjs', '.js', '.ts']
|
||||||
|
}),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
typescript({
|
sucrase({
|
||||||
typescript: require('typescript')
|
transforms: ['typescript']
|
||||||
})
|
})
|
||||||
],
|
]
|
||||||
experimentalCodeSplitting: true
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
12
runtime/internal/Sapper.svelte
Normal file
12
runtime/internal/Sapper.svelte
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<script>
|
||||||
|
import { setContext } from 'svelte';
|
||||||
|
import { CONTEXT_KEY } from './shared';
|
||||||
|
|
||||||
|
export let Root;
|
||||||
|
export let props;
|
||||||
|
export let session;
|
||||||
|
|
||||||
|
setContext(CONTEXT_KEY, session);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Root {...props}/>
|
||||||
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
runtime/internal/layout.svelte
Normal file
1
runtime/internal/layout.svelte
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<slot></slot>
|
||||||
10
runtime/internal/shared.mjs
Normal file
10
runtime/internal/shared.mjs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
|
export const stores = {
|
||||||
|
preloading: writable(false),
|
||||||
|
page: writable(null)
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CONTEXT_KEY = {};
|
||||||
|
|
||||||
|
export const preload = () => ({});
|
||||||
349
runtime/src/app/app.ts
Normal file
349
runtime/src/app/app.ts
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
import { writable } from 'svelte/store.mjs';
|
||||||
|
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 {
|
||||||
|
Target,
|
||||||
|
ScrollPosition,
|
||||||
|
Component,
|
||||||
|
Redirect,
|
||||||
|
ComponentLoader,
|
||||||
|
ComponentConstructor,
|
||||||
|
Route,
|
||||||
|
Query,
|
||||||
|
Page
|
||||||
|
} from './types';
|
||||||
|
import goto from './goto';
|
||||||
|
|
||||||
|
declare const __SAPPER__;
|
||||||
|
export const initial_data = typeof __SAPPER__ !== 'undefined' && __SAPPER__;
|
||||||
|
|
||||||
|
let ready = false;
|
||||||
|
let root_component: Component;
|
||||||
|
let current_token: {};
|
||||||
|
let root_preloaded: Promise<any>;
|
||||||
|
let current_branch = [];
|
||||||
|
|
||||||
|
const session = writable(initial_data && initial_data.session);
|
||||||
|
|
||||||
|
let $session;
|
||||||
|
let session_dirty: boolean;
|
||||||
|
|
||||||
|
session.subscribe(async value => {
|
||||||
|
$session = value;
|
||||||
|
|
||||||
|
if (!ready) return;
|
||||||
|
session_dirty = true;
|
||||||
|
|
||||||
|
const target = select_target(new URL(location.href));
|
||||||
|
|
||||||
|
const token = current_token = {};
|
||||||
|
const { redirect, props, branch } = await hydrate_target(target);
|
||||||
|
if (token !== current_token) return; // a secondary navigation happened while we were loading
|
||||||
|
|
||||||
|
await render(redirect, branch, props, target.page);
|
||||||
|
});
|
||||||
|
|
||||||
|
export let prefetching: {
|
||||||
|
href: string;
|
||||||
|
promise: Promise<{ redirect?: Redirect, data?: any }>;
|
||||||
|
} = null;
|
||||||
|
export function set_prefetching(href, promise) {
|
||||||
|
prefetching = { href, promise };
|
||||||
|
}
|
||||||
|
|
||||||
|
export let store;
|
||||||
|
export function set_store(fn) {
|
||||||
|
store = fn(initial_data.store);
|
||||||
|
}
|
||||||
|
|
||||||
|
export let target: Node;
|
||||||
|
export function set_target(element) {
|
||||||
|
target = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export let uid = 1;
|
||||||
|
export function set_uid(n) {
|
||||||
|
uid = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
export let cid: number;
|
||||||
|
export function set_cid(n) {
|
||||||
|
cid = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _history = typeof history !== 'undefined' ? history : {
|
||||||
|
pushState: (state: any, title: string, href: string) => {},
|
||||||
|
replaceState: (state: any, title: string, href: string) => {},
|
||||||
|
scrollRestoration: ''
|
||||||
|
};
|
||||||
|
export { _history as history };
|
||||||
|
|
||||||
|
export const scroll_history: Record<string, ScrollPosition> = {};
|
||||||
|
|
||||||
|
export function 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;
|
||||||
|
|
||||||
|
const path = url.pathname.slice(initial_data.baseUrl.length);
|
||||||
|
|
||||||
|
// avoid accidental clashes between server routes and page routes
|
||||||
|
if (ignore.some(pattern => pattern.test(path))) return;
|
||||||
|
|
||||||
|
for (let i = 0; i < routes.length; i += 1) {
|
||||||
|
const route = routes[i];
|
||||||
|
|
||||||
|
const match = route.pattern.exec(path);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const query: Query = extract_query(url.search);
|
||||||
|
const part = route.parts[route.parts.length - 1];
|
||||||
|
const params = part.params ? part.params(match) : {};
|
||||||
|
|
||||||
|
const page = { path, query, params };
|
||||||
|
|
||||||
|
return { href: url.href, route, match, page };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function 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,
|
||||||
|
y: pageYOffset
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function navigate(target: Target, id: number, noscroll?: boolean, hash?: string): Promise<any> {
|
||||||
|
if (id) {
|
||||||
|
// popstate or initial navigation
|
||||||
|
cid = id;
|
||||||
|
} else {
|
||||||
|
const current_scroll = scroll_state();
|
||||||
|
|
||||||
|
// clicked on a link. preserve scroll state
|
||||||
|
scroll_history[cid] = current_scroll;
|
||||||
|
|
||||||
|
id = cid = ++uid;
|
||||||
|
scroll_history[cid] = noscroll ? current_scroll : { x: 0, y: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
cid = id;
|
||||||
|
|
||||||
|
if (root_component) stores.preloading.set(true);
|
||||||
|
|
||||||
|
const loaded = prefetching && prefetching.href === target.href ?
|
||||||
|
prefetching.promise :
|
||||||
|
hydrate_target(target);
|
||||||
|
|
||||||
|
prefetching = null;
|
||||||
|
|
||||||
|
const token = current_token = {};
|
||||||
|
const { redirect, props, branch } = await loaded;
|
||||||
|
if (token !== current_token) return; // a secondary navigation happened while we were loading
|
||||||
|
|
||||||
|
await render(redirect, branch, props, target.page);
|
||||||
|
if (document.activeElement) document.activeElement.blur();
|
||||||
|
|
||||||
|
if (!noscroll) {
|
||||||
|
let scroll = scroll_history[id];
|
||||||
|
|
||||||
|
if (hash) {
|
||||||
|
// scroll is an element id (from a hash), we need to compute y.
|
||||||
|
const deep_linked = document.getElementById(hash.slice(1));
|
||||||
|
|
||||||
|
if (deep_linked) {
|
||||||
|
scroll = {
|
||||||
|
x: 0,
|
||||||
|
y: deep_linked.getBoundingClientRect().top
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scroll_history[cid] = scroll;
|
||||||
|
if (scroll) scrollTo(scroll.x, scroll.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function render(redirect: Redirect, branch: any[], props: any, page: Page) {
|
||||||
|
if (redirect) return goto(redirect.location, { replaceState: true });
|
||||||
|
|
||||||
|
stores.page.set(page);
|
||||||
|
stores.preloading.set(false);
|
||||||
|
|
||||||
|
if (root_component) {
|
||||||
|
root_component.$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');
|
||||||
|
|
||||||
|
if (start && end) {
|
||||||
|
while (start.nextSibling !== end) detach(start.nextSibling);
|
||||||
|
detach(start);
|
||||||
|
detach(end);
|
||||||
|
}
|
||||||
|
|
||||||
|
root_component = new App({
|
||||||
|
target,
|
||||||
|
props,
|
||||||
|
hydrate: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
current_branch = branch;
|
||||||
|
ready = true;
|
||||||
|
session_dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function hydrate_target(target: Target): Promise<{
|
||||||
|
redirect?: Redirect;
|
||||||
|
props?: any;
|
||||||
|
branch?: Array<{ Component: ComponentConstructor, preload: (page) => Promise<any>, segment: string }>;
|
||||||
|
}> {
|
||||||
|
const { route, page } = target;
|
||||||
|
const segments = page.path.split('/').filter(Boolean);
|
||||||
|
|
||||||
|
let redirect: Redirect = null;
|
||||||
|
|
||||||
|
const props = { error: null, status: 200, segments: [segments[0]] };
|
||||||
|
|
||||||
|
const preload_context = {
|
||||||
|
fetch: (url: string, opts?: any) => fetch(url, opts),
|
||||||
|
redirect: (statusCode: number, location: string) => {
|
||||||
|
if (redirect && (redirect.statusCode !== statusCode || redirect.location !== location)) {
|
||||||
|
throw new Error(`Conflicting redirects`);
|
||||||
|
}
|
||||||
|
redirect = { statusCode, location };
|
||||||
|
},
|
||||||
|
error: (status: number, error: Error | string) => {
|
||||||
|
props.error = typeof error === 'string' ? new Error(error) : error;
|
||||||
|
props.status = status;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!root_preloaded) {
|
||||||
|
root_preloaded = initial_data.preloaded[0] || root_preload.call(preload_context, {
|
||||||
|
path: page.path,
|
||||||
|
query: page.query,
|
||||||
|
params: {}
|
||||||
|
}, $session);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 j = l++;
|
||||||
|
|
||||||
|
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]) {
|
||||||
|
preloaded = preload
|
||||||
|
? await preload.call(preload_context, {
|
||||||
|
path: page.path,
|
||||||
|
query: page.query,
|
||||||
|
params: part.params ? part.params(target.match) : {}
|
||||||
|
}, $session)
|
||||||
|
: {};
|
||||||
|
} else {
|
||||||
|
preloaded = initial_data.preloaded[i + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return (props[`level${j}`] = { component, props: preloaded, segment, part: part.i });
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
props.error = error;
|
||||||
|
props.status = 500;
|
||||||
|
branch = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return { redirect, props, branch };
|
||||||
|
}
|
||||||
|
|
||||||
|
function load_css(chunk: string) {
|
||||||
|
const href = `client/${chunk}`;
|
||||||
|
if (document.querySelector(`link[href="${href}"]`)) return;
|
||||||
|
|
||||||
|
return new Promise((fulfil, reject) => {
|
||||||
|
const link = document.createElement('link');
|
||||||
|
link.rel = 'stylesheet';
|
||||||
|
link.href = href;
|
||||||
|
|
||||||
|
link.onload = () => fulfil();
|
||||||
|
link.onerror = reject;
|
||||||
|
|
||||||
|
document.head.appendChild(link);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function load_component(component: ComponentLoader): Promise<{
|
||||||
|
default: ComponentConstructor,
|
||||||
|
preload?: (input: any) => any
|
||||||
|
}> {
|
||||||
|
// TODO this is temporary — once placeholders are
|
||||||
|
// always rewritten, scratch the ternary
|
||||||
|
const promises: Array<Promise<any>> = (typeof component.css === 'string' ? [] : component.css.map(load_css));
|
||||||
|
promises.unshift(component.js());
|
||||||
|
return Promise.all(promises).then(values => values[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function detach(node: Node) {
|
||||||
|
node.parentNode.removeChild(node);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { history, select_route, navigate, cid } from '../app';
|
import { history, select_target, navigate, cid } from '../app';
|
||||||
|
|
||||||
export default function goto(href: string, opts = { replaceState: false }) {
|
export default function goto(href: string, opts = { replaceState: false }) {
|
||||||
const target = select_route(new URL(href, document.baseURI));
|
const target = select_target(new URL(href, document.baseURI));
|
||||||
|
|
||||||
if (target) {
|
if (target) {
|
||||||
history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
|
history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
|
||||||
12
runtime/src/app/index.ts
Normal file
12
runtime/src/app/index.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { getContext } from 'svelte';
|
||||||
|
import { CONTEXT_KEY, stores } from '@sapper/internal/shared';
|
||||||
|
|
||||||
|
export const preloading = { subscribe: stores.preloading.subscribe };
|
||||||
|
export const page = { subscribe: stores.page.subscribe };
|
||||||
|
|
||||||
|
export const getSession = () => getContext(CONTEXT_KEY);
|
||||||
|
|
||||||
|
export { default as start } from './start/index';
|
||||||
|
export { default as goto } from './goto/index';
|
||||||
|
export { default as prefetch } from './prefetch/index';
|
||||||
|
export { default as prefetchRoutes } from './prefetchRoutes/index';
|
||||||
14
runtime/src/app/prefetch/index.ts
Normal file
14
runtime/src/app/prefetch/index.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { select_target, prefetching, set_prefetching, hydrate_target } from '../app';
|
||||||
|
import { Target } from '../types';
|
||||||
|
|
||||||
|
export default function prefetch(href: string) {
|
||||||
|
const target: Target = select_target(new URL(href, document.baseURI));
|
||||||
|
|
||||||
|
if (target) {
|
||||||
|
if (!prefetching || href !== prefetching.href) {
|
||||||
|
set_prefetching(href, hydrate_target(target));
|
||||||
|
}
|
||||||
|
|
||||||
|
return prefetching.promise;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { components, pages, load_component } from "../app";
|
import { components, routes } from '@sapper/internal/manifest-client';
|
||||||
|
import { load_component } from '../app';
|
||||||
|
|
||||||
export default function prefetchRoutes(pathnames: string[]) {
|
export default function prefetchRoutes(pathnames: string[]) {
|
||||||
return pages
|
return routes
|
||||||
.filter(route => {
|
.filter(pathnames
|
||||||
if (!pathnames) return true;
|
? route => pathnames.some(pathname => route.pattern.test(pathname))
|
||||||
return pathnames.some(pathname => route.pattern.test(pathname));
|
: () => true
|
||||||
})
|
)
|
||||||
.reduce((promise: Promise<any>, route) => promise.then(() => {
|
.reduce((promise: Promise<any>, route) => promise.then(() => {
|
||||||
return Promise.all(route.parts.map(part => part && load_component(components[part.i])));
|
return Promise.all(route.parts.map(part => part && load_component(components[part.i])));
|
||||||
}), Promise.resolve());
|
}), Promise.resolve());
|
||||||
@@ -5,26 +5,23 @@ import {
|
|||||||
navigate,
|
navigate,
|
||||||
scroll_history,
|
scroll_history,
|
||||||
scroll_state,
|
scroll_state,
|
||||||
select_route,
|
select_target,
|
||||||
set_store,
|
handle_error,
|
||||||
set_target,
|
set_target,
|
||||||
uid,
|
uid,
|
||||||
set_uid,
|
set_uid,
|
||||||
set_cid
|
set_cid
|
||||||
} from '../app';
|
} from '../app';
|
||||||
import prefetch from '../prefetch/index';
|
import prefetch from '../prefetch/index';
|
||||||
import { Store, ScrollPosition } from '../types';
|
|
||||||
|
|
||||||
export default function start(opts: {
|
export default function start(opts: {
|
||||||
target: Node,
|
target: Node
|
||||||
store?: (data: any) => Store
|
|
||||||
}) {
|
}) {
|
||||||
if ('scrollRestoration' in history) {
|
if ('scrollRestoration' in history) {
|
||||||
history.scrollRestoration = 'manual';
|
history.scrollRestoration = 'manual';
|
||||||
}
|
}
|
||||||
|
|
||||||
set_target(opts.target);
|
set_target(opts.target);
|
||||||
if (opts.store) set_store(opts.store);
|
|
||||||
|
|
||||||
addEventListener('click', handle_click);
|
addEventListener('click', handle_click);
|
||||||
addEventListener('popstate', handle_popstate);
|
addEventListener('popstate', handle_popstate);
|
||||||
@@ -38,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_route(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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +95,7 @@ function handle_click(event: MouseEvent) {
|
|||||||
// Don't handle hash changes
|
// Don't handle hash changes
|
||||||
if (url.pathname === location.pathname && url.search === location.search) return;
|
if (url.pathname === location.pathname && url.search === location.search) return;
|
||||||
|
|
||||||
const target = select_route(url);
|
const target = select_target(url);
|
||||||
if (target) {
|
if (target) {
|
||||||
const noscroll = a.hasAttribute('sapper-noscroll');
|
const noscroll = a.hasAttribute('sapper-noscroll');
|
||||||
navigate(target, null, noscroll, url.hash);
|
navigate(target, null, noscroll, url.hash);
|
||||||
@@ -119,7 +118,7 @@ function handle_popstate(event: PopStateEvent) {
|
|||||||
|
|
||||||
if (event.state) {
|
if (event.state) {
|
||||||
const url = new URL(location.href);
|
const url = new URL(location.href);
|
||||||
const target = select_route(url);
|
const target = select_target(url);
|
||||||
if (target) {
|
if (target) {
|
||||||
navigate(target, event.state.id);
|
navigate(target, event.state.id);
|
||||||
} else {
|
} else {
|
||||||
@@ -8,21 +8,14 @@ type Child = {
|
|||||||
component?: Component;
|
component?: Component;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RootProps = {
|
|
||||||
path: string;
|
|
||||||
params: Record<string, string>;
|
|
||||||
query: Record<string, string>;
|
|
||||||
child: Child;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface ComponentConstructor {
|
export interface ComponentConstructor {
|
||||||
new (options: { target: Node, data: any, store: Store, hydrate: boolean }): Component;
|
new (options: { target: Node, props: any, hydrate: boolean }): Component;
|
||||||
preload: (props: { params: Params, query: Query }) => Promise<any>;
|
preload: (props: { params: Params, query: Query }) => Promise<any>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface Component {
|
export interface Component {
|
||||||
set: (data: any) => void;
|
$set: (data: any) => void;
|
||||||
destroy: () => void;
|
$destroy: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComponentLoader = {
|
export type ComponentLoader = {
|
||||||
@@ -30,7 +23,7 @@ export type ComponentLoader = {
|
|||||||
css: string[]
|
css: string[]
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Page = {
|
export type Route = {
|
||||||
pattern: RegExp;
|
pattern: RegExp;
|
||||||
parts: Array<{
|
parts: Array<{
|
||||||
i: number;
|
i: number;
|
||||||
@@ -42,7 +35,7 @@ export type Manifest = {
|
|||||||
ignore: RegExp[];
|
ignore: RegExp[];
|
||||||
root: ComponentConstructor;
|
root: ComponentConstructor;
|
||||||
error: () => Promise<{ default: ComponentConstructor }>;
|
error: () => Promise<{ default: ComponentConstructor }>;
|
||||||
pages: Page[]
|
pages: Route[]
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ScrollPosition = {
|
export type ScrollPosition = {
|
||||||
@@ -51,11 +44,10 @@ export type ScrollPosition = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type Target = {
|
export type Target = {
|
||||||
url: URL;
|
href: string;
|
||||||
path: string;
|
route: Route;
|
||||||
page: Page;
|
|
||||||
match: RegExpExecArray;
|
match: RegExpExecArray;
|
||||||
query: Record<string, string | string[]>;
|
page: Page;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Redirect = {
|
export type Redirect = {
|
||||||
@@ -63,6 +55,8 @@ export type Redirect = {
|
|||||||
location: string;
|
location: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Store = {
|
export type Page = {
|
||||||
get: () => any;
|
path: string;
|
||||||
}
|
params: Record<string, string>;
|
||||||
|
query: Record<string, string | string[]>;
|
||||||
|
};
|
||||||
1
runtime/src/server/constants.js
Normal file
1
runtime/src/server/constants.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const IGNORE = '__SAPPER__IGNORE__';
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
import * as fs from 'fs';
|
import { writable } from 'svelte/store.mjs';
|
||||||
import * as path from 'path';
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
import cookie from 'cookie';
|
import cookie from 'cookie';
|
||||||
import devalue from 'devalue';
|
import devalue from 'devalue';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
import { URL, resolve } from 'url';
|
import URL from 'url';
|
||||||
import { build_dir, dev, src_dir, IGNORE } from '../placeholders';
|
import { IGNORE } from '../constants';
|
||||||
import { Manifest, Page, Props, Req, Res, Store } from './types';
|
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 App from '@sapper/internal/App.svelte';
|
||||||
|
|
||||||
export function get_page_handler(
|
export function get_page_handler(
|
||||||
manifest: Manifest,
|
manifest: Manifest,
|
||||||
store_getter: (req: Req, res: Res) => Store
|
session_getter: (req: Req, res: Res) => any
|
||||||
) {
|
) {
|
||||||
const get_build_info = dev
|
const get_build_info = dev
|
||||||
? () => JSON.parse(fs.readFileSync(path.join(build_dir, 'build.json'), 'utf-8'))
|
? () => JSON.parse(fs.readFileSync(path.join(build_dir, 'build.json'), 'utf-8'))
|
||||||
@@ -34,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,
|
||||||
@@ -48,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;
|
||||||
|
|
||||||
@@ -77,7 +81,7 @@ export function get_page_handler(
|
|||||||
res.setHeader('Link', link);
|
res.setHeader('Link', link);
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = store_getter ? store_getter(req, res) : null;
|
const session = session_getter(req, res);
|
||||||
|
|
||||||
let redirect: { statusCode: number, location: string };
|
let redirect: { statusCode: number, location: string };
|
||||||
let preload_error: { statusCode: number, message: Error | string };
|
let preload_error: { statusCode: number, message: Error | string };
|
||||||
@@ -94,7 +98,7 @@ export function get_page_handler(
|
|||||||
preload_error = { statusCode, message };
|
preload_error = { statusCode, message };
|
||||||
},
|
},
|
||||||
fetch: (url: string, opts?: any) => {
|
fetch: (url: string, opts?: any) => {
|
||||||
const parsed = new URL(url, `http://127.0.0.1:${process.env.PORT}${req.baseUrl ? req.baseUrl + '/' :''}`);
|
const parsed = new URL.URL(url, `http://127.0.0.1:${process.env.PORT}${req.baseUrl ? req.baseUrl + '/' :''}`);
|
||||||
|
|
||||||
if (opts) {
|
if (opts) {
|
||||||
opts = Object.assign({}, opts);
|
opts = Object.assign({}, opts);
|
||||||
@@ -105,7 +109,7 @@ export function get_page_handler(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (include_cookies) {
|
if (include_cookies) {
|
||||||
if (!opts.headers) opts.headers = {};
|
opts.headers = Object.assign({}, opts.headers);
|
||||||
|
|
||||||
const cookies = Object.assign(
|
const cookies = Object.assign(
|
||||||
{},
|
{},
|
||||||
@@ -128,35 +132,39 @@ export function get_page_handler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return fetch(parsed.href, opts);
|
return fetch(parsed.href, opts);
|
||||||
},
|
}
|
||||||
store
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let preloaded;
|
let preloaded;
|
||||||
let match;
|
let match;
|
||||||
|
let params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const root_preloaded = manifest.root.preload
|
const root_preloaded = manifest.root_preload
|
||||||
? manifest.root.preload.call(preload_context, {
|
? manifest.root_preload.call(preload_context, {
|
||||||
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;
|
||||||
|
|
||||||
return part.component.preload
|
// the deepest level is used below, to initialise the store
|
||||||
? part.component.preload.call(preload_context, {
|
params = part.params ? part.params(match) : {};
|
||||||
|
|
||||||
|
return part.preload
|
||||||
|
? part.preload.call(preload_context, {
|
||||||
path: req.path,
|
path: req.path,
|
||||||
query: req.query,
|
query: req.query,
|
||||||
params: part.params ? part.params(match) : {}
|
params
|
||||||
})
|
}, session)
|
||||||
: {};
|
: {};
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -169,7 +177,7 @@ export function get_page_handler(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (redirect) {
|
if (redirect) {
|
||||||
const location = resolve(req.baseUrl || '/', redirect.location);
|
const location = URL.resolve(req.baseUrl || '/', redirect.location);
|
||||||
|
|
||||||
res.statusCode = redirect.statusCode;
|
res.statusCode = redirect.statusCode;
|
||||||
res.setHeader('Location', location);
|
res.setHeader('Location', location);
|
||||||
@@ -183,67 +191,67 @@ export function get_page_handler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const serialized = {
|
|
||||||
preloaded: `[${preloaded.map(data => try_serialize(data)).join(',')}]`,
|
|
||||||
store: store && try_serialize(store.get())
|
|
||||||
};
|
|
||||||
|
|
||||||
const segments = req.path.split('/').filter(Boolean);
|
const segments = req.path.split('/').filter(Boolean);
|
||||||
|
|
||||||
const props: Props = {
|
// TODO make this less confusing
|
||||||
path: req.path,
|
const layout_segments = [segments[0]];
|
||||||
query: req.query,
|
let l = 1;
|
||||||
params: {},
|
|
||||||
child: null
|
|
||||||
};
|
|
||||||
|
|
||||||
if (error) {
|
page.parts.forEach((part, i) => {
|
||||||
props.error = error instanceof Error ? error : { message: error };
|
layout_segments[l] = segments[i + 1];
|
||||||
props.status = status;
|
if (!part) return null;
|
||||||
}
|
l++;
|
||||||
|
|
||||||
const data = Object.assign({}, props, preloaded[0], {
|
|
||||||
params: {},
|
|
||||||
child: {
|
|
||||||
segment: segments[0]
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let level = data.child;
|
const props = {
|
||||||
if (isSWIndexHtml) {
|
segments: layout_segments,
|
||||||
level.props = Object.assign({}, props, {
|
status: error ? status : 200,
|
||||||
params: {}
|
error: error ? error instanceof Error ? error : { message: error } : null,
|
||||||
})
|
session: writable(session),
|
||||||
} else {
|
level0: {
|
||||||
|
props: preloaded[0]
|
||||||
|
},
|
||||||
|
level1: {
|
||||||
|
segment: segments[0],
|
||||||
|
props: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!is_service_worker_index) {
|
||||||
|
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;
|
||||||
|
|
||||||
const get_params = part.params || (() => ({}));
|
props[`level${l++}`] = {
|
||||||
|
|
||||||
Object.assign(level, {
|
|
||||||
component: part.component,
|
component: part.component,
|
||||||
props: Object.assign({}, props, {
|
props: preloaded[i + 1] || {},
|
||||||
params: get_params(match)
|
segment: segments[i]
|
||||||
}, preloaded[i + 1])
|
|
||||||
});
|
|
||||||
|
|
||||||
level.props.child = <Props["child"]>{
|
|
||||||
segment: segments[i + 1]
|
|
||||||
};
|
};
|
||||||
level = level.props.child;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { html, head, css } = manifest.root.render(data, {
|
stores.page.set({
|
||||||
store
|
path: req.path,
|
||||||
|
query: req.query,
|
||||||
|
params: params
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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__={${[
|
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.store && `store:${serialized.store}`
|
serialized.session && `session:${serialized.session}`
|
||||||
].filter(Boolean).join(',')}};`;
|
].filter(Boolean).join(',')}};`;
|
||||||
|
|
||||||
if (has_service_worker) {
|
if (has_service_worker) {
|
||||||
@@ -302,6 +310,7 @@ export function get_page_handler(
|
|||||||
res.statusCode = status;
|
res.statusCode = status;
|
||||||
res.end(body);
|
res.end(body);
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
|
console.log(err);
|
||||||
if (error) {
|
if (error) {
|
||||||
// we encountered an error while rendering the error page — oops
|
// we encountered an error while rendering the error page — oops
|
||||||
res.statusCode = 500;
|
res.statusCode = 500;
|
||||||
@@ -321,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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,10 +345,11 @@ function read_template(dir = build_dir) {
|
|||||||
return fs.readFileSync(`${dir}/template.html`, 'utf-8');
|
return fs.readFileSync(`${dir}/template.html`, 'utf-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
function try_serialize(data: any) {
|
function try_serialize(data: any, fail?: (err) => void) {
|
||||||
try {
|
try {
|
||||||
return devalue(data);
|
return devalue(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (fail) fail(err);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IGNORE } from '../placeholders';
|
import { IGNORE } from '../constants';
|
||||||
import { Req, Res, ServerRoute } from './types';
|
import { Req, Res, ServerRoute } from './types';
|
||||||
|
|
||||||
export function get_server_route_handler(routes: ServerRoute[]) {
|
export function get_server_route_handler(routes: ServerRoute[]) {
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
import * as fs from 'fs';
|
import fs from 'fs';
|
||||||
import * as path from 'path';
|
import path from 'path';
|
||||||
import { build_dir, dev, manifest, IGNORE } from '../placeholders';
|
import { build_dir, dev, manifest } from '@sapper/internal/manifest-server';
|
||||||
import { Handler, Req, Res, Store } from './types';
|
import { Handler, Req, Res } from './types';
|
||||||
import { get_server_route_handler } from './get_server_route_handler';
|
import { get_server_route_handler } from './get_server_route_handler';
|
||||||
import { get_page_handler } from './get_page_handler';
|
import { get_page_handler } from './get_page_handler';
|
||||||
import { lookup } from './mime';
|
import { lookup } from './mime';
|
||||||
|
import { IGNORE } from '../constants';
|
||||||
|
|
||||||
export default function middleware(opts: {
|
export default function middleware(opts: {
|
||||||
store?: (req: Req, res: Res) => Store,
|
session?: (req: Req, res: Res) => any,
|
||||||
ignore?: any
|
ignore?: any
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const { store, ignore } = opts;
|
const { session, ignore } = opts;
|
||||||
|
|
||||||
let emitted_basepath = false;
|
let emitted_basepath = false;
|
||||||
|
|
||||||
@@ -51,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'
|
||||||
@@ -73,7 +69,7 @@ export default function middleware(opts: {
|
|||||||
|
|
||||||
get_server_route_handler(manifest.server_routes),
|
get_server_route_handler(manifest.server_routes),
|
||||||
|
|
||||||
get_page_handler(manifest, store)
|
get_page_handler(manifest, session || noop)
|
||||||
].filter(Boolean));
|
].filter(Boolean));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,3 +137,5 @@ export function serve({ prefix, pathname, cache_control }: {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function noop(){}
|
||||||
@@ -12,6 +12,7 @@ export type Page = {
|
|||||||
name: string;
|
name: string;
|
||||||
component: Component;
|
component: Component;
|
||||||
params?: (match: RegExpMatchArray) => Record<string, string>;
|
params?: (match: RegExpMatchArray) => Record<string, string>;
|
||||||
|
preload?: (data: any) => any | Promise<any>;
|
||||||
}>
|
}>
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -19,19 +20,13 @@ export type Manifest = {
|
|||||||
server_routes: ServerRoute[];
|
server_routes: ServerRoute[];
|
||||||
pages: Page[];
|
pages: Page[];
|
||||||
root: Component;
|
root: Component;
|
||||||
|
root_preload?: (data: any) => any | Promise<any>;
|
||||||
error: Component;
|
error: Component;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Handler = (req: Req, res: Res, next: () => void) => void;
|
export type Handler = (req: Req, res: Res, next: () => void) => void;
|
||||||
|
|
||||||
export type Store = {
|
|
||||||
get: () => any
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
path: string;
|
|
||||||
query: Record<string, string>;
|
|
||||||
params: Record<string, string>;
|
|
||||||
error?: { message: string };
|
error?: { message: string };
|
||||||
status?: number;
|
status?: number;
|
||||||
child: {
|
child: {
|
||||||
@@ -60,10 +55,9 @@ export interface Res extends ServerResponse {
|
|||||||
export { ServerResponse };
|
export { ServerResponse };
|
||||||
|
|
||||||
interface Component {
|
interface Component {
|
||||||
render: (data: any, opts: { store: Store }) => {
|
render: (data: any) => {
|
||||||
head: string;
|
head: string;
|
||||||
css: { code: string, map: any };
|
css: { code: string, map: any };
|
||||||
html: string
|
html: string
|
||||||
},
|
}
|
||||||
preload: (data: any) => any | Promise<any>
|
|
||||||
}
|
}
|
||||||
@@ -1,14 +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 { rimraf, mkdirp } from './utils/fs_utils';
|
||||||
|
|
||||||
type Opts = {
|
type Opts = {
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
@@ -26,7 +26,7 @@ export async function build({
|
|||||||
cwd,
|
cwd,
|
||||||
src = 'src',
|
src = 'src',
|
||||||
routes = 'src/routes',
|
routes = 'src/routes',
|
||||||
output = '__sapper__',
|
output = 'src/node_modules/@sapper',
|
||||||
static: static_files = 'static',
|
static: static_files = 'static',
|
||||||
dest = '__sapper__/build',
|
dest = '__sapper__/build',
|
||||||
|
|
||||||
@@ -42,14 +42,17 @@ export async function build({
|
|||||||
routes = path.resolve(cwd, routes);
|
routes = path.resolve(cwd, routes);
|
||||||
output = path.resolve(cwd, output);
|
output = path.resolve(cwd, output);
|
||||||
static_files = path.resolve(cwd, static_files);
|
static_files = path.resolve(cwd, static_files);
|
||||||
dest = path.resolve(cwd, dest);
|
|
||||||
|
|
||||||
if (legacy && bundler === 'webpack') {
|
if (legacy && bundler === 'webpack') {
|
||||||
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(dest, '**/*'));
|
rimraf(output);
|
||||||
mkdirp.sync(`${dest}/client`);
|
mkdirp(output);
|
||||||
|
copy_runtime(output);
|
||||||
|
|
||||||
|
rimraf(dest);
|
||||||
|
mkdirp(`${dest}/client`);
|
||||||
copy_shimport(dest);
|
copy_shimport(dest);
|
||||||
|
|
||||||
// minify src/template.html
|
// minify src/template.html
|
||||||
@@ -67,8 +70,8 @@ export async function build({
|
|||||||
|
|
||||||
const manifest_data = create_manifest_data(routes);
|
const manifest_data = create_manifest_data(routes);
|
||||||
|
|
||||||
// create src/manifest/client.js and src/manifest/server.js
|
// create src/node_modules/@sapper/app.mjs and server.mjs
|
||||||
create_main_manifests({
|
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';
|
||||||
@@ -15,6 +13,8 @@ import { copy_shimport } from './utils/copy_shimport';
|
|||||||
import { ManifestData, FatalEvent, ErrorEvent, ReadyEvent, InvalidEvent } from '../interfaces';
|
import { ManifestData, FatalEvent, ErrorEvent, ReadyEvent, InvalidEvent } from '../interfaces';
|
||||||
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 { rimraf, mkdirp } from './utils/fs_utils';
|
||||||
|
|
||||||
type Opts = {
|
type Opts = {
|
||||||
cwd?: string,
|
cwd?: string,
|
||||||
@@ -72,7 +72,7 @@ class Watcher extends EventEmitter {
|
|||||||
cwd = '.',
|
cwd = '.',
|
||||||
src = 'src',
|
src = 'src',
|
||||||
routes = 'src/routes',
|
routes = 'src/routes',
|
||||||
output = '__sapper__',
|
output = 'src/node_modules/@sapper',
|
||||||
static: static_files = 'static',
|
static: static_files = 'static',
|
||||||
dest = '__sapper__/dev',
|
dest = '__sapper__/dev',
|
||||||
'dev-port': dev_port,
|
'dev-port': dev_port,
|
||||||
@@ -144,8 +144,13 @@ 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(dest);
|
|
||||||
mkdirp.sync(`${dest}/client`);
|
rimraf(output);
|
||||||
|
mkdirp(output);
|
||||||
|
copy_runtime(output);
|
||||||
|
|
||||||
|
rimraf(dest);
|
||||||
|
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);
|
||||||
@@ -157,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,
|
||||||
@@ -185,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,
|
||||||
@@ -194,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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -403,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', {
|
||||||
@@ -484,7 +490,7 @@ function watch_dir(
|
|||||||
let watch: any;
|
let watch: any;
|
||||||
let closed = false;
|
let closed = false;
|
||||||
|
|
||||||
import('cheap-watch').then(CheapWatch => {
|
import('cheap-watch').then(({ default: CheapWatch }) => {
|
||||||
if (closed) return;
|
if (closed) return;
|
||||||
|
|
||||||
watch = new CheapWatch({ dir, filter, debounce: 50 });
|
watch = new CheapWatch({ dir, filter, debounce: 50 });
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
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 ports from 'port-authority';
|
import * as ports from 'port-authority';
|
||||||
import clean_html from './utils/clean_html';
|
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,
|
||||||
@@ -20,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));
|
||||||
}
|
}
|
||||||
@@ -46,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}`;
|
||||||
@@ -84,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;
|
||||||
@@ -106,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 => {
|
||||||
@@ -138,37 +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 urls: URL[] = [];
|
// 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 cleaned = clean_html(body);
|
const q = yootils.queue(8);
|
||||||
|
|
||||||
const base_match = /<base ([\s\S]+?)>/m.exec(cleaned);
|
const base_match = /<base ([\s\S]+?)>/m.exec(cleaned);
|
||||||
const base_href = base_match && get_href(base_match[1]);
|
const base_href = base_match && get_href(base_match[1]);
|
||||||
const base = resolve(url.href, base_href);
|
const base = resolve(url.href, base_href);
|
||||||
|
|
||||||
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) {
|
||||||
urls.push(url);
|
q.add(() => handle(url));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(urls.map(handle));
|
await q.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,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]);
|
||||||
}
|
}
|
||||||
21
src/api/utils/copy_runtime.ts
Normal file
21
src/api/utils/copy_runtime.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { mkdirp } from './fs_utils';
|
||||||
|
|
||||||
|
const runtime = [
|
||||||
|
'app.mjs',
|
||||||
|
'server.mjs',
|
||||||
|
'internal/shared.mjs',
|
||||||
|
'internal/layout.svelte',
|
||||||
|
'internal/error.svelte'
|
||||||
|
].map(file => ({
|
||||||
|
file,
|
||||||
|
source: fs.readFileSync(path.join(__dirname, `../runtime/${file}`), 'utf-8')
|
||||||
|
}));
|
||||||
|
|
||||||
|
export function copy_runtime(output: string) {
|
||||||
|
runtime.forEach(({ file, source }) => {
|
||||||
|
mkdirp(path.dirname(`${output}/${file}`));
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
54
src/cli.ts
54
src/cli.ts
@@ -10,7 +10,7 @@ const prog = sade('sapper').version(pkg.version);
|
|||||||
|
|
||||||
if (process.argv[2] === 'start') {
|
if (process.argv[2] === 'start') {
|
||||||
// remove this in a future version
|
// remove this in a future version
|
||||||
console.error(colors.bold.red(`'sapper start' has been removed`));
|
console.error(colors.bold().red(`'sapper start' has been removed`));
|
||||||
console.error(`Use 'node [build_dir]' instead`);
|
console.error(`Use 'node [build_dir]' instead`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ prog.command('dev')
|
|||||||
.option('--src', 'Source directory', 'src')
|
.option('--src', 'Source directory', 'src')
|
||||||
.option('--routes', 'Routes directory', 'src/routes')
|
.option('--routes', 'Routes directory', 'src/routes')
|
||||||
.option('--static', 'Static files directory', 'static')
|
.option('--static', 'Static files directory', 'static')
|
||||||
.option('--output', 'Sapper output directory', '__sapper__')
|
.option('--output', 'Sapper output directory', 'src/node_modules/@sapper')
|
||||||
.option('--build-dir', 'Development build directory', '__sapper__/dev')
|
.option('--build-dir', 'Development build directory', '__sapper__/dev')
|
||||||
.action(async (opts: {
|
.action(async (opts: {
|
||||||
port: number,
|
port: number,
|
||||||
@@ -74,7 +74,7 @@ prog.command('dev')
|
|||||||
|
|
||||||
watcher.on('ready', async (event: ReadyEvent) => {
|
watcher.on('ready', async (event: ReadyEvent) => {
|
||||||
if (first) {
|
if (first) {
|
||||||
console.log(colors.bold.cyan(`> Listening on http://localhost:${event.port}`));
|
console.log(colors.bold().cyan(`> Listening on http://localhost:${event.port}`));
|
||||||
if (opts.open) {
|
if (opts.open) {
|
||||||
const { exec } = await import('child_process');
|
const { exec } = await import('child_process');
|
||||||
exec(`open http://localhost:${event.port}`);
|
exec(`open http://localhost:${event.port}`);
|
||||||
@@ -85,22 +85,30 @@ prog.command('dev')
|
|||||||
|
|
||||||
watcher.on('invalid', (event: InvalidEvent) => {
|
watcher.on('invalid', (event: InvalidEvent) => {
|
||||||
const changed = event.changed.map(filename => path.relative(process.cwd(), filename)).join(', ');
|
const changed = event.changed.map(filename => path.relative(process.cwd(), filename)).join(', ');
|
||||||
console.log(`\n${colors.bold.cyan(changed)} changed. rebuilding...`);
|
console.log(`\n${colors.bold().cyan(changed)} changed. rebuilding...`);
|
||||||
});
|
});
|
||||||
|
|
||||||
watcher.on('error', (event: ErrorEvent) => {
|
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) => {
|
||||||
console.log(colors.bold.red(`> ${event.message}`));
|
console.log(colors.bold().red(`> ${event.message}`));
|
||||||
if (event.log) console.log(event.log);
|
if (event.log) console.log(event.log);
|
||||||
});
|
});
|
||||||
|
|
||||||
watcher.on('build', (event: BuildEvent) => {
|
watcher.on('build', (event: BuildEvent) => {
|
||||||
if (event.errors.length) {
|
if (event.errors.length) {
|
||||||
console.log(colors.bold.red(`✗ ${event.type}`));
|
console.log(colors.bold().red(`✗ ${event.type}`));
|
||||||
|
|
||||||
event.errors.filter(e => !e.duplicate).forEach(error => {
|
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));
|
||||||
@@ -112,7 +120,7 @@ prog.command('dev')
|
|||||||
console.log(`${hidden} duplicate ${hidden === 1 ? 'error' : 'errors'} hidden\n`);
|
console.log(`${hidden} duplicate ${hidden === 1 ? 'error' : 'errors'} hidden\n`);
|
||||||
}
|
}
|
||||||
} else if (event.warnings.length) {
|
} else if (event.warnings.length) {
|
||||||
console.log(colors.bold.yellow(`• ${event.type}`));
|
console.log(colors.bold().yellow(`• ${event.type}`));
|
||||||
|
|
||||||
event.warnings.filter(e => !e.duplicate).forEach(warning => {
|
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));
|
||||||
@@ -124,11 +132,12 @@ prog.command('dev')
|
|||||||
console.log(`${hidden} duplicate ${hidden === 1 ? 'warning' : 'warnings'} hidden\n`);
|
console.log(`${hidden} duplicate ${hidden === 1 ? 'warning' : 'warnings'} hidden\n`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(`${colors.bold.green(`✔ ${event.type}`)} ${colors.gray(`(${format_milliseconds(event.duration)})`)}`);
|
console.log(`${colors.bold().green(`✔ ${event.type}`)} ${colors.gray(`(${format_milliseconds(event.duration)})`)}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(colors.bold.red(`> ${err.message}`));
|
console.log(colors.bold().red(`> ${err.message}`));
|
||||||
|
console.log(colors.gray(err.stack));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -141,7 +150,7 @@ prog.command('build [dest]')
|
|||||||
.option('--cwd', 'Current working directory', '.')
|
.option('--cwd', 'Current working directory', '.')
|
||||||
.option('--src', 'Source directory', 'src')
|
.option('--src', 'Source directory', 'src')
|
||||||
.option('--routes', 'Routes directory', 'src/routes')
|
.option('--routes', 'Routes directory', 'src/routes')
|
||||||
.option('--output', 'Sapper output directory', '__sapper__')
|
.option('--output', 'Sapper output directory', 'src/node_modules/@sapper')
|
||||||
.example(`build custom-dir -p 4567`)
|
.example(`build custom-dir -p 4567`)
|
||||||
.action(async (dest = '__sapper__/build', opts: {
|
.action(async (dest = '__sapper__/build', opts: {
|
||||||
port: string,
|
port: string,
|
||||||
@@ -168,9 +177,10 @@ prog.command('build [dest]')
|
|||||||
require('./server/server.js');
|
require('./server/server.js');
|
||||||
`.replace(/^\t+/gm, '').trim());
|
`.replace(/^\t+/gm, '').trim());
|
||||||
|
|
||||||
console.error(`\n> Finished in ${elapsed(start)}. Type ${colors.bold.cyan(`node ${dest}`)} to run the app.`);
|
console.error(`\n> Finished in ${elapsed(start)}. Type ${colors.bold().cyan(`node ${dest}`)} to run the app.`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(`${colors.bold.red(`> ${err.message}`)}`);
|
console.log(`${colors.bold().red(`> ${err.message}`)}`);
|
||||||
|
console.log(colors.gray(err.stack));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -186,7 +196,7 @@ prog.command('export [dest]')
|
|||||||
.option('--src', 'Source directory', 'src')
|
.option('--src', 'Source directory', 'src')
|
||||||
.option('--routes', 'Routes directory', 'src/routes')
|
.option('--routes', 'Routes directory', 'src/routes')
|
||||||
.option('--static', 'Static files directory', 'static')
|
.option('--static', 'Static files directory', 'static')
|
||||||
.option('--output', 'Sapper output directory', '__sapper__')
|
.option('--output', 'Sapper output directory', 'src/node_modules/@sapper')
|
||||||
.option('--build-dir', 'Intermediate build directory', '__sapper__/build')
|
.option('--build-dir', 'Intermediate build directory', '__sapper__/build')
|
||||||
.action(async (dest = '__sapper__/export', opts: {
|
.action(async (dest = '__sapper__/export', opts: {
|
||||||
build: boolean,
|
build: boolean,
|
||||||
@@ -220,24 +230,24 @@ prog.command('export [dest]')
|
|||||||
timeout: opts.timeout,
|
timeout: opts.timeout,
|
||||||
|
|
||||||
oninfo: event => {
|
oninfo: event => {
|
||||||
console.log(colors.bold.cyan(`> ${event.message}`));
|
console.log(colors.bold().cyan(`> ${event.message}`));
|
||||||
},
|
},
|
||||||
|
|
||||||
onfile: event => {
|
onfile: event => {
|
||||||
const size_color = event.size > 150000 ? colors.bold.red : event.size > 50000 ? colors.bold.yellow : colors.bold.gray;
|
const size_color = event.size > 150000 ? colors.bold().red : event.size > 50000 ? colors.bold().yellow : colors.bold().gray;
|
||||||
const size_label = size_color(left_pad(pb(event.size), 10));
|
const size_label = size_color(left_pad(pb(event.size), 10));
|
||||||
|
|
||||||
const file_label = event.status === 200
|
const file_label = event.status === 200
|
||||||
? event.file
|
? event.file
|
||||||
: colors.bold[event.status >= 400 ? 'red' : 'yellow'](`(${event.status}) ${event.file}`);
|
: colors.bold()[event.status >= 400 ? 'red' : 'yellow'](`(${event.status}) ${event.file}`);
|
||||||
|
|
||||||
console.log(`${size_label} ${file_label}`);
|
console.log(`${size_label} ${file_label}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.error(`\n> Finished in ${elapsed(start)}. Type ${colors.bold.cyan(`npx serve ${dest}`)} to run the app.`);
|
console.error(`\n> Finished in ${elapsed(start)}. Type ${colors.bold().cyan(`npx serve ${dest}`)} to run the app.`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(colors.bold.red(`> ${err.message}`));
|
console.error(colors.bold().red(`> ${err.message}`));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -266,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';
|
||||||
291
src/core/create_app.ts
Normal file
291
src/core/create_app.ts
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { posixify, stringify, walk, write_if_changed } from '../utils';
|
||||||
|
import { Page, PageComponent, ManifestData } from '../interfaces';
|
||||||
|
|
||||||
|
export function create_app({
|
||||||
|
bundler,
|
||||||
|
manifest_data,
|
||||||
|
dev_port,
|
||||||
|
dev,
|
||||||
|
cwd,
|
||||||
|
src,
|
||||||
|
dest,
|
||||||
|
routes,
|
||||||
|
output
|
||||||
|
}: {
|
||||||
|
bundler: string,
|
||||||
|
manifest_data: ManifestData;
|
||||||
|
dev_port?: number;
|
||||||
|
dev: boolean;
|
||||||
|
cwd: string;
|
||||||
|
src: string;
|
||||||
|
dest: string;
|
||||||
|
routes: string;
|
||||||
|
output: string
|
||||||
|
}) {
|
||||||
|
if (!fs.existsSync(output)) fs.mkdirSync(output);
|
||||||
|
|
||||||
|
const path_to_routes = path.relative(`${output}/internal`, routes);
|
||||||
|
|
||||||
|
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 }: {
|
||||||
|
manifest_data: ManifestData;
|
||||||
|
output: string;
|
||||||
|
client_files: string[];
|
||||||
|
static_files: string;
|
||||||
|
}) {
|
||||||
|
let files: string[] = ['service-worker-index.html'];
|
||||||
|
|
||||||
|
if (fs.existsSync(static_files)) {
|
||||||
|
files = files.concat(walk(static_files));
|
||||||
|
} else {
|
||||||
|
// TODO remove in a future version
|
||||||
|
if (fs.existsSync('assets')) {
|
||||||
|
throw new Error(`As of Sapper 0.21, the assets/ directory should become static/`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let code = `
|
||||||
|
// This file is generated by Sapper — do not edit it!
|
||||||
|
export const timestamp = ${Date.now()};
|
||||||
|
|
||||||
|
export const files = [\n\t${files.map((x: string) => stringify(x)).join(',\n\t')}\n];
|
||||||
|
export { files as assets }; // legacy
|
||||||
|
|
||||||
|
export const shell = [\n\t${client_files.map((x: string) => stringify(x)).join(',\n\t')}\n];
|
||||||
|
|
||||||
|
export const routes = [\n\t${manifest_data.pages.map((r: Page) => `{ pattern: ${r.pattern} }`).join(',\n\t')}\n];
|
||||||
|
`.replace(/^\t\t/gm, '').trim();
|
||||||
|
|
||||||
|
write_if_changed(`${output}/service-worker.js`, code);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generate_client_manifest(
|
||||||
|
manifest_data: ManifestData,
|
||||||
|
path_to_routes: string,
|
||||||
|
bundler: string,
|
||||||
|
dev: boolean,
|
||||||
|
dev_port?: number
|
||||||
|
) {
|
||||||
|
const page_ids = new Set(manifest_data.pages.map(page =>
|
||||||
|
page.pattern.toString()));
|
||||||
|
|
||||||
|
const server_routes_to_ignore = manifest_data.server_routes.filter(route =>
|
||||||
|
!page_ids.has(route.pattern.toString()));
|
||||||
|
|
||||||
|
const component_indexes: Record<string, number> = {};
|
||||||
|
|
||||||
|
const components = `[
|
||||||
|
${manifest_data.components.map((component, i) => {
|
||||||
|
const annotation = bundler === 'webpack'
|
||||||
|
? `/* webpackChunkName: "${component.name}" */ `
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const source = get_file(path_to_routes, component);
|
||||||
|
|
||||||
|
component_indexes[component.name] = i;
|
||||||
|
|
||||||
|
return `{
|
||||||
|
js: () => import(${annotation}${stringify(source)}),
|
||||||
|
css: "__SAPPER_CSS_PLACEHOLDER:${stringify(component.file, false)}__"
|
||||||
|
}`;
|
||||||
|
}).join(',\n\t\t\t\t')}
|
||||||
|
]`.replace(/^\t/gm, '');
|
||||||
|
|
||||||
|
let needs_decode = false;
|
||||||
|
|
||||||
|
let routes = `[
|
||||||
|
${manifest_data.pages.map(page => `{
|
||||||
|
// ${page.parts[page.parts.length - 1].component.file}
|
||||||
|
pattern: ${page.pattern},
|
||||||
|
parts: [
|
||||||
|
${page.parts.map(part => {
|
||||||
|
if (part === null) return 'null';
|
||||||
|
|
||||||
|
if (part.params.length > 0) {
|
||||||
|
needs_decode = true;
|
||||||
|
const props = part.params.map((param, i) => `${param}: d(match[${i + 1}])`);
|
||||||
|
return `{ i: ${component_indexes[part.component.name]}, params: match => ({ ${props.join(', ')} }) }`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `{ i: ${component_indexes[part.component.name]} }`;
|
||||||
|
}).join(',\n\t\t\t\t\t\t')}
|
||||||
|
]
|
||||||
|
}`).join(',\n\n\t\t\t\t')}
|
||||||
|
]`.replace(/^\t/gm, '');
|
||||||
|
|
||||||
|
if (needs_decode) {
|
||||||
|
routes = `(d => ${routes})(decodeURIComponent)`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
// This file is generated by Sapper — do not edit it!
|
||||||
|
export { default as Root } from '${stringify(get_file(path_to_routes, manifest_data.root), false)}';
|
||||||
|
export { preload as root_preload } from '${manifest_data.root.has_preload ? stringify(get_file(path_to_routes, manifest_data.root), false) : './shared'}';
|
||||||
|
export { default as ErrorComponent } from '${stringify(get_file(path_to_routes, manifest_data.error), false)}';
|
||||||
|
|
||||||
|
export const ignore = [${server_routes_to_ignore.map(route => route.pattern).join(', ')}];
|
||||||
|
|
||||||
|
export const components = ${components};
|
||||||
|
|
||||||
|
export const routes = ${routes};
|
||||||
|
|
||||||
|
${dev ? `if (typeof window !== 'undefined') {
|
||||||
|
import(${stringify(posixify(path.resolve(__dirname, '../sapper-dev-client.js')))}).then(client => {
|
||||||
|
client.connect(${dev_port});
|
||||||
|
});
|
||||||
|
}` : ''}
|
||||||
|
`.replace(/^\t{2}/gm, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function generate_server_manifest(
|
||||||
|
manifest_data: ManifestData,
|
||||||
|
path_to_routes: string,
|
||||||
|
cwd: string,
|
||||||
|
src: string,
|
||||||
|
dest: string,
|
||||||
|
dev: boolean
|
||||||
|
) {
|
||||||
|
const imports = [].concat(
|
||||||
|
manifest_data.server_routes.map((route, i) =>
|
||||||
|
`import * as route_${i} from ${stringify(posixify(`${path_to_routes}/${route.file}`))};`),
|
||||||
|
manifest_data.components.map((component, i) =>
|
||||||
|
`import component_${i}${component.has_preload ? `, { preload as preload_${i} }` : ''} from ${stringify(get_file(path_to_routes, component))};`),
|
||||||
|
`import root${manifest_data.root.has_preload ? `, { preload as root_preload }` : ''} from ${stringify(get_file(path_to_routes, manifest_data.root))};`,
|
||||||
|
`import error from ${stringify(get_file(path_to_routes, manifest_data.error))};`
|
||||||
|
);
|
||||||
|
|
||||||
|
const component_lookup: Record<string, number> = {};
|
||||||
|
manifest_data.components.forEach((component, i) => {
|
||||||
|
component_lookup[component.name] = i;
|
||||||
|
});
|
||||||
|
|
||||||
|
let code = `
|
||||||
|
`.replace(/^\t\t/gm, '').trim();
|
||||||
|
|
||||||
|
const build_dir = posixify(path.relative(cwd, dest));
|
||||||
|
const src_dir = posixify(path.relative(cwd, src));
|
||||||
|
|
||||||
|
return `
|
||||||
|
// This file is generated by Sapper — do not edit it!
|
||||||
|
${imports.join('\n')}
|
||||||
|
|
||||||
|
const d = decodeURIComponent;
|
||||||
|
|
||||||
|
export const manifest = {
|
||||||
|
server_routes: [
|
||||||
|
${manifest_data.server_routes.map((route, i) => `{
|
||||||
|
// ${route.file}
|
||||||
|
pattern: ${route.pattern},
|
||||||
|
handlers: route_${i},
|
||||||
|
params: ${route.params.length > 0
|
||||||
|
? `match => ({ ${route.params.map((param, i) => `${param}: d(match[${i + 1}])`).join(', ')} })`
|
||||||
|
: `() => ({})`}
|
||||||
|
}`).join(',\n\n\t\t\t\t')}
|
||||||
|
],
|
||||||
|
|
||||||
|
pages: [
|
||||||
|
${manifest_data.pages.map(page => `{
|
||||||
|
// ${page.parts[page.parts.length - 1].component.file}
|
||||||
|
pattern: ${page.pattern},
|
||||||
|
parts: [
|
||||||
|
${page.parts.map(part => {
|
||||||
|
if (part === null) return 'null';
|
||||||
|
|
||||||
|
const props = [
|
||||||
|
`name: "${part.component.name}"`,
|
||||||
|
`file: ${stringify(part.component.file)}`,
|
||||||
|
`component: component_${component_lookup[part.component.name]}`,
|
||||||
|
part.component.has_preload && `preload: preload_${component_lookup[part.component.name]}`
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
if (part.params.length > 0) {
|
||||||
|
const params = part.params.map((param, i) => `${param}: d(match[${i + 1}])`);
|
||||||
|
props.push(`params: match => ({ ${params.join(', ')} })`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `{ ${props.join(', ')} }`;
|
||||||
|
}).join(',\n\t\t\t\t\t\t')}
|
||||||
|
]
|
||||||
|
}`).join(',\n\n\t\t\t\t')}
|
||||||
|
],
|
||||||
|
|
||||||
|
root,
|
||||||
|
root_preload${manifest_data.root.has_preload ? '' : `: () => {}`},
|
||||||
|
error
|
||||||
|
};
|
||||||
|
|
||||||
|
export const build_dir = ${JSON.stringify(build_dir)};
|
||||||
|
|
||||||
|
export const src_dir = ${JSON.stringify(src_dir)};
|
||||||
|
|
||||||
|
export const dev = ${dev ? 'true' : 'false'};
|
||||||
|
`.replace(/^\t{2}/gm, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 `./${component.type}.svelte`;
|
||||||
|
return posixify(`${path_to_routes}/${component.file}`);
|
||||||
|
}
|
||||||
@@ -47,12 +47,13 @@ export default class RollupResult implements CompileResult {
|
|||||||
} else {
|
} else {
|
||||||
for (const name in compiler.input) {
|
for (const name in compiler.input) {
|
||||||
const file = compiler.input[name];
|
const file = compiler.input[name];
|
||||||
this.assets[name] = compiler.chunks.find(chunk => file in chunk.modules).fileName;
|
const chunk = compiler.chunks.find(chunk => file in chunk.modules);
|
||||||
|
if (chunk) this.assets[name] = chunk.fileName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.summary = compiler.chunks.map(chunk => {
|
this.summary = compiler.chunks.map(chunk => {
|
||||||
const size_color = chunk.code.length > 150000 ? colors.bold.red : chunk.code.length > 50000 ? colors.bold.yellow : colors.bold.white;
|
const size_color = chunk.code.length > 150000 ? colors.bold().red : chunk.code.length > 50000 ? colors.bold().yellow : colors.bold().white;
|
||||||
const size_label = left_pad(pb(chunk.code.length), 10);
|
const size_label = left_pad(pb(chunk.code.length), 10);
|
||||||
|
|
||||||
const lines = [size_color(`${size_label} ${chunk.fileName}`)];
|
const lines = [size_color(`${size_label} ${chunk.fileName}`)];
|
||||||
|
|||||||
@@ -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;
|
||||||
}, {})
|
}, {})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,6 +161,8 @@ export default function extract_css(client_result: CompileResult, components: Pa
|
|||||||
|
|
||||||
// recursively find the chunks this component depends on
|
// recursively find the chunks this component depends on
|
||||||
entry_chunk_dependencies.forEach(chunk => {
|
entry_chunk_dependencies.forEach(chunk => {
|
||||||
|
if (!chunk) return; // TODO why does this happen?
|
||||||
|
|
||||||
chunk.imports.forEach(file => {
|
chunk.imports.forEach(file => {
|
||||||
entry_chunk_dependencies.add(lookup.get(file));
|
entry_chunk_dependencies.add(lookup.get(file));
|
||||||
});
|
});
|
||||||
@@ -182,7 +184,8 @@ export default function extract_css(client_result: CompileResult, components: Pa
|
|||||||
|
|
||||||
if (!chunk) {
|
if (!chunk) {
|
||||||
// this should never happen!
|
// this should never happen!
|
||||||
throw new Error(`Could not find chunk that owns ${component.file}`);
|
return;
|
||||||
|
// throw new Error(`Could not find chunk that owns ${component.file}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const chunk_dependencies: Set<Chunk> = new Set([chunk]);
|
const chunk_dependencies: Set<Chunk> = new Set([chunk]);
|
||||||
@@ -190,6 +193,8 @@ export default function extract_css(client_result: CompileResult, components: Pa
|
|||||||
|
|
||||||
// recursively find the chunks this component depends on
|
// recursively find the chunks this component depends on
|
||||||
chunk_dependencies.forEach(chunk => {
|
chunk_dependencies.forEach(chunk => {
|
||||||
|
if (!chunk) return; // TODO why does this happen?
|
||||||
|
|
||||||
chunk.imports.forEach(file => {
|
chunk.imports.forEach(file => {
|
||||||
chunk_dependencies.add(lookup.get(file));
|
chunk_dependencies.add(lookup.get(file));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,22 +1,48 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
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)) {
|
||||||
throw new Error(`As of Sapper 0.21, the routes/ directory should become src/routes/`);
|
throw new Error(`As of Sapper 0.21, the routes/ directory should become src/routes/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function has_preload(file: string) {
|
||||||
|
const source = fs.readFileSync(path.join(cwd, file), 'utf-8');
|
||||||
|
|
||||||
|
if (/preload/.test(source)) {
|
||||||
|
try {
|
||||||
|
const { vars } = svelte.compile(source.replace(/<style\b[^>]*>[^]*?<\/style>/g, ''), { generate: false });
|
||||||
|
return vars.some((variable: any) => variable.module && variable.export_name === 'preload');
|
||||||
|
} catch (err) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const components: PageComponent[] = [];
|
const components: PageComponent[] = [];
|
||||||
const pages: Page[] = [];
|
const pages: Page[] = [];
|
||||||
const server_routes: ServerRoute[] = [];
|
const server_routes: ServerRoute[] = [];
|
||||||
|
|
||||||
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
|
||||||
|
};
|
||||||
|
|
||||||
|
const default_error: PageComponent = {
|
||||||
|
default: true,
|
||||||
|
type: 'error',
|
||||||
|
name: '_default_error',
|
||||||
|
file: null,
|
||||||
|
has_preload: false
|
||||||
};
|
};
|
||||||
|
|
||||||
function walk(
|
function walk(
|
||||||
@@ -43,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)) {
|
||||||
@@ -57,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,
|
||||||
@@ -103,11 +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${ext}`)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (component) components.push(component);
|
if (component) components.push(component);
|
||||||
@@ -123,28 +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)
|
||||||
};
|
};
|
||||||
|
|
||||||
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 {
|
||||||
@@ -158,14 +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${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
|
||||||
@@ -196,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
|
||||||
|
|||||||
@@ -1,243 +0,0 @@
|
|||||||
import * as fs from 'fs';
|
|
||||||
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({
|
|
||||||
bundler,
|
|
||||||
manifest_data,
|
|
||||||
dev_port,
|
|
||||||
dev,
|
|
||||||
cwd,
|
|
||||||
src,
|
|
||||||
dest,
|
|
||||||
routes,
|
|
||||||
output
|
|
||||||
}: {
|
|
||||||
bundler: string,
|
|
||||||
manifest_data: ManifestData;
|
|
||||||
dev_port?: number;
|
|
||||||
dev: boolean;
|
|
||||||
cwd: string;
|
|
||||||
src: string;
|
|
||||||
dest: string;
|
|
||||||
routes: string;
|
|
||||||
output: string
|
|
||||||
}) {
|
|
||||||
if (!fs.existsSync(output)) fs.mkdirSync(output);
|
|
||||||
|
|
||||||
const path_to_routes = path.relative(output, routes);
|
|
||||||
|
|
||||||
const client_manifest = generate_client(manifest_data, path_to_routes, bundler, dev, dev_port);
|
|
||||||
const server_manifest = generate_server(manifest_data, path_to_routes, cwd, src, dest, dev);
|
|
||||||
|
|
||||||
write_if_changed(
|
|
||||||
`${output}/_layout.html`,
|
|
||||||
`<svelte:component this={child.component} {...child.props}/>`
|
|
||||||
);
|
|
||||||
write_if_changed(`${output}/client.js`, client_manifest);
|
|
||||||
write_if_changed(`${output}/server.js`, server_manifest);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function create_serviceworker_manifest({ manifest_data, output, client_files, static_files }: {
|
|
||||||
manifest_data: ManifestData;
|
|
||||||
output: string;
|
|
||||||
client_files: string[];
|
|
||||||
static_files: string;
|
|
||||||
}) {
|
|
||||||
let files: string[] = ['/service-worker-index.html'];
|
|
||||||
|
|
||||||
if (fs.existsSync(static_files)) {
|
|
||||||
files = files.concat(walk(static_files));
|
|
||||||
} else {
|
|
||||||
// TODO remove in a future version
|
|
||||||
if (fs.existsSync('assets')) {
|
|
||||||
throw new Error(`As of Sapper 0.21, the assets/ directory should become static/`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let code = `
|
|
||||||
// This file is generated by Sapper — do not edit it!
|
|
||||||
export const timestamp = ${Date.now()};
|
|
||||||
|
|
||||||
export const files = [\n\t${files.map((x: string) => stringify(x)).join(',\n\t')}\n];
|
|
||||||
export { files as assets }; // legacy
|
|
||||||
|
|
||||||
export const shell = [\n\t${client_files.map((x: string) => stringify(x)).join(',\n\t')}\n];
|
|
||||||
|
|
||||||
export const routes = [\n\t${manifest_data.pages.map((r: Page) => `{ pattern: ${r.pattern} }`).join(',\n\t')}\n];
|
|
||||||
`.replace(/^\t\t/gm, '').trim();
|
|
||||||
|
|
||||||
write_if_changed(`${output}/service-worker.js`, code);
|
|
||||||
}
|
|
||||||
|
|
||||||
function generate_client(
|
|
||||||
manifest_data: ManifestData,
|
|
||||||
path_to_routes: string,
|
|
||||||
bundler: string,
|
|
||||||
dev: boolean,
|
|
||||||
dev_port?: number
|
|
||||||
) {
|
|
||||||
const template_file = path.resolve(__dirname, '../templates/client.js');
|
|
||||||
const template = fs.readFileSync(template_file, 'utf-8');
|
|
||||||
|
|
||||||
const page_ids = new Set(manifest_data.pages.map(page =>
|
|
||||||
page.pattern.toString()));
|
|
||||||
|
|
||||||
const server_routes_to_ignore = manifest_data.server_routes.filter(route =>
|
|
||||||
!page_ids.has(route.pattern.toString()));
|
|
||||||
|
|
||||||
const component_indexes: Record<string, number> = {};
|
|
||||||
|
|
||||||
const components = `[
|
|
||||||
${manifest_data.components.map((component, i) => {
|
|
||||||
const annotation = bundler === 'webpack'
|
|
||||||
? `/* webpackChunkName: "${component.name}" */ `
|
|
||||||
: '';
|
|
||||||
|
|
||||||
const source = get_file(path_to_routes, component);
|
|
||||||
|
|
||||||
component_indexes[component.name] = i;
|
|
||||||
|
|
||||||
return `{
|
|
||||||
js: () => import(${annotation}${stringify(source)}),
|
|
||||||
css: "__SAPPER_CSS_PLACEHOLDER:${stringify(component.file, false)}__"
|
|
||||||
}`;
|
|
||||||
}).join(',\n\t\t')}
|
|
||||||
]`.replace(/^\t/gm, '').trim();
|
|
||||||
|
|
||||||
let needs_decode = false;
|
|
||||||
|
|
||||||
let pages = `[
|
|
||||||
${manifest_data.pages.map(page => `{
|
|
||||||
// ${page.parts[page.parts.length - 1].component.file}
|
|
||||||
pattern: ${page.pattern},
|
|
||||||
parts: [
|
|
||||||
${page.parts.map(part => {
|
|
||||||
if (part === null) return 'null';
|
|
||||||
|
|
||||||
if (part.params.length > 0) {
|
|
||||||
needs_decode = true;
|
|
||||||
const props = part.params.map((param, i) => `${param}: d(match[${i + 1}])`);
|
|
||||||
return `{ i: ${component_indexes[part.component.name]}, params: match => ({ ${props.join(', ')} }) }`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `{ i: ${component_indexes[part.component.name]} }`;
|
|
||||||
}).join(',\n\t\t\t\t')}
|
|
||||||
]
|
|
||||||
}`).join(',\n\n\t\t')}
|
|
||||||
]`.replace(/^\t/gm, '').trim();
|
|
||||||
|
|
||||||
if (needs_decode) {
|
|
||||||
pages = `(d => ${pages})(decodeURIComponent)`
|
|
||||||
}
|
|
||||||
|
|
||||||
let footer = '';
|
|
||||||
|
|
||||||
if (dev) {
|
|
||||||
const sapper_dev_client = posixify(
|
|
||||||
path.resolve(__dirname, '../sapper-dev-client.js')
|
|
||||||
);
|
|
||||||
|
|
||||||
footer = `
|
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
import(${stringify(sapper_dev_client)}).then(client => {
|
|
||||||
client.connect(${dev_port});
|
|
||||||
});
|
|
||||||
}`.replace(/^\t{3}/gm, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
return `// This file is generated by Sapper — do not edit it!\n` + template
|
|
||||||
.replace('__ROOT__', stringify(get_file(path_to_routes, manifest_data.root), false))
|
|
||||||
.replace('__ERROR__', stringify(posixify(`${path_to_routes}/_error.html`), false))
|
|
||||||
.replace('__IGNORE__', `[${server_routes_to_ignore.map(route => route.pattern).join(', ')}]`)
|
|
||||||
.replace('__COMPONENTS__', components)
|
|
||||||
.replace('__PAGES__', pages) +
|
|
||||||
footer;
|
|
||||||
}
|
|
||||||
|
|
||||||
function generate_server(
|
|
||||||
manifest_data: ManifestData,
|
|
||||||
path_to_routes: string,
|
|
||||||
cwd: string,
|
|
||||||
src: string,
|
|
||||||
dest: string,
|
|
||||||
dev: boolean
|
|
||||||
) {
|
|
||||||
const template_file = path.resolve(__dirname, '../templates/server.js');
|
|
||||||
const template = fs.readFileSync(template_file, 'utf-8');
|
|
||||||
|
|
||||||
const imports = [].concat(
|
|
||||||
manifest_data.server_routes.map(route =>
|
|
||||||
`import * as __${route.name} from ${stringify(posixify(`${path_to_routes}/${route.file}`))};`),
|
|
||||||
manifest_data.components.map(component =>
|
|
||||||
`import __${component.name} from ${stringify(get_file(path_to_routes, component))};`),
|
|
||||||
`import root from ${stringify(get_file(path_to_routes, manifest_data.root))};`,
|
|
||||||
`import error from ${stringify(posixify(`${path_to_routes}/_error.html`))};`
|
|
||||||
);
|
|
||||||
|
|
||||||
let code = `
|
|
||||||
${imports.join('\n')}
|
|
||||||
|
|
||||||
const d = decodeURIComponent;
|
|
||||||
|
|
||||||
export const manifest = {
|
|
||||||
server_routes: [
|
|
||||||
${manifest_data.server_routes.map(route => `{
|
|
||||||
// ${route.file}
|
|
||||||
pattern: ${route.pattern},
|
|
||||||
handlers: __${route.name},
|
|
||||||
params: ${route.params.length > 0
|
|
||||||
? `match => ({ ${route.params.map((param, i) => `${param}: d(match[${i + 1}])`).join(', ')} })`
|
|
||||||
: `() => ({})`}
|
|
||||||
}`).join(',\n\n\t\t\t\t')}
|
|
||||||
],
|
|
||||||
|
|
||||||
pages: [
|
|
||||||
${manifest_data.pages.map(page => `{
|
|
||||||
// ${page.parts[page.parts.length - 1].component.file}
|
|
||||||
pattern: ${page.pattern},
|
|
||||||
parts: [
|
|
||||||
${page.parts.map(part => {
|
|
||||||
if (part === null) return 'null';
|
|
||||||
|
|
||||||
const props = [
|
|
||||||
`name: "${part.component.name}"`,
|
|
||||||
`file: ${stringify(part.component.file)}`,
|
|
||||||
`component: __${part.component.name}`
|
|
||||||
];
|
|
||||||
|
|
||||||
if (part.params.length > 0) {
|
|
||||||
const params = part.params.map((param, i) => `${param}: d(match[${i + 1}])`);
|
|
||||||
props.push(`params: match => ({ ${params.join(', ')} })`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return `{ ${props.join(', ')} }`;
|
|
||||||
}).join(',\n\t\t\t\t\t\t')}
|
|
||||||
]
|
|
||||||
}`).join(',\n\n\t\t\t\t')}
|
|
||||||
],
|
|
||||||
|
|
||||||
root,
|
|
||||||
|
|
||||||
error
|
|
||||||
};`.replace(/^\t\t/gm, '').trim();
|
|
||||||
|
|
||||||
const build_dir = posixify(path.relative(cwd, dest));
|
|
||||||
const src_dir = posixify(path.relative(cwd, src));
|
|
||||||
|
|
||||||
return `// This file is generated by Sapper — do not edit it!\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 get_file(path_to_routes: string, component: PageComponent) {
|
|
||||||
if (component.default) {
|
|
||||||
return `./_layout.html`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return posixify(`${path_to_routes}/${component.file}`);
|
|
||||||
}
|
|
||||||
@@ -19,14 +19,18 @@ export type Template = {
|
|||||||
stream: (req, res, data: Record<string, string | Promise<string>>) => void;
|
stream: (req, res, data: Record<string, string | Promise<string>>) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Store = {
|
export type WritableStore<T> = {
|
||||||
get: () => any;
|
set: (value: T) => void;
|
||||||
|
update: (fn: (value: T) => T) => void;
|
||||||
|
subscribe: (fn: (T: any) => void) => () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PageComponent = {
|
export type PageComponent = {
|
||||||
default?: boolean;
|
default?: boolean;
|
||||||
|
type?: string;
|
||||||
name: string;
|
name: string;
|
||||||
file: string;
|
file: string;
|
||||||
|
has_preload: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Page = {
|
export type Page = {
|
||||||
@@ -52,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[];
|
||||||
@@ -64,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 = {
|
||||||
|
|||||||
1
src/types.d.ts
vendored
Normal file
1
src/types.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
declare module 'svelte/compiler';
|
||||||
@@ -1,389 +0,0 @@
|
|||||||
import RootComponent from '__ROOT__';
|
|
||||||
import ErrorComponent from '__ERROR__';
|
|
||||||
import {
|
|
||||||
Target,
|
|
||||||
ScrollPosition,
|
|
||||||
Component,
|
|
||||||
Redirect,
|
|
||||||
ComponentLoader,
|
|
||||||
ComponentConstructor,
|
|
||||||
RootProps,
|
|
||||||
Page
|
|
||||||
} from './types';
|
|
||||||
import goto from './goto';
|
|
||||||
|
|
||||||
const ignore = __IGNORE__;
|
|
||||||
export const components: ComponentLoader[] = __COMPONENTS__;
|
|
||||||
export const pages: Page[] = __PAGES__;
|
|
||||||
|
|
||||||
let ready = false;
|
|
||||||
let root_component: Component;
|
|
||||||
let segments: string[] = [];
|
|
||||||
let current_token: {};
|
|
||||||
let root_preload: Promise<any>;
|
|
||||||
let root_data: any;
|
|
||||||
|
|
||||||
const root_props: RootProps = {
|
|
||||||
path: null,
|
|
||||||
params: null,
|
|
||||||
query: null,
|
|
||||||
child: {
|
|
||||||
segment: null,
|
|
||||||
component: null,
|
|
||||||
props: {}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export let prefetching: {
|
|
||||||
href: string;
|
|
||||||
promise: Promise<{ redirect?: Redirect, data?: any, nullable_depth?: number }>;
|
|
||||||
} = null;
|
|
||||||
export function set_prefetching(href, promise) {
|
|
||||||
prefetching = { href, promise };
|
|
||||||
}
|
|
||||||
|
|
||||||
export let store;
|
|
||||||
export function set_store(fn) {
|
|
||||||
store = fn(initial_data.store);
|
|
||||||
}
|
|
||||||
|
|
||||||
export let target: Node;
|
|
||||||
export function set_target(element) {
|
|
||||||
target = element;
|
|
||||||
}
|
|
||||||
|
|
||||||
export let uid = 1;
|
|
||||||
export function set_uid(n) {
|
|
||||||
uid = n;
|
|
||||||
}
|
|
||||||
|
|
||||||
export let cid: number;
|
|
||||||
export function set_cid(n) {
|
|
||||||
cid = n;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const initial_data = typeof __SAPPER__ !== 'undefined' && __SAPPER__;
|
|
||||||
|
|
||||||
const _history = typeof history !== 'undefined' ? history : {
|
|
||||||
pushState: (state: any, title: string, href: string) => {},
|
|
||||||
replaceState: (state: any, title: string, href: string) => {},
|
|
||||||
scrollRestoration: ''
|
|
||||||
};
|
|
||||||
export { _history as history };
|
|
||||||
|
|
||||||
export const scroll_history: Record<string, ScrollPosition> = {};
|
|
||||||
|
|
||||||
export function select_route(url: URL): Target {
|
|
||||||
if (url.origin !== location.origin) return null;
|
|
||||||
if (!url.pathname.startsWith(initial_data.baseUrl)) return null;
|
|
||||||
|
|
||||||
const path = url.pathname.slice(initial_data.baseUrl.length);
|
|
||||||
|
|
||||||
// avoid accidental clashes between server routes and pages
|
|
||||||
if (ignore.some(pattern => pattern.test(path))) return;
|
|
||||||
|
|
||||||
for (let i = 0; i < pages.length; i += 1) {
|
|
||||||
const page = pages[i];
|
|
||||||
|
|
||||||
const match = page.pattern.exec(path);
|
|
||||||
if (match) {
|
|
||||||
const query: Record<string, string | string[]> = Object.create(null);
|
|
||||||
if (url.search.length > 0) {
|
|
||||||
url.search.slice(1).split('&').forEach(searchParam => {
|
|
||||||
let [, key, value] = /([^=]*)(?:=(.*))?/.exec(decodeURIComponent(searchParam));
|
|
||||||
value = (value || '').replace(/\+/g, ' ');
|
|
||||||
if (typeof query[key] === 'string') query[key] = [<string>query[key]];
|
|
||||||
if (typeof query[key] === 'object') query[key].push(value);
|
|
||||||
else query[key] = value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return { url, path, page, match, query };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function scroll_state() {
|
|
||||||
return {
|
|
||||||
x: pageXOffset,
|
|
||||||
y: pageYOffset
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function navigate(target: Target, id: number, noscroll?: boolean, hash?: string): Promise<any> {
|
|
||||||
let scroll: ScrollPosition;
|
|
||||||
if (id) {
|
|
||||||
// popstate or initial navigation
|
|
||||||
cid = id;
|
|
||||||
} else {
|
|
||||||
const current_scroll = scroll_state();
|
|
||||||
|
|
||||||
// clicked on a link. preserve scroll state
|
|
||||||
scroll_history[cid] = current_scroll;
|
|
||||||
|
|
||||||
id = cid = ++uid;
|
|
||||||
scroll_history[cid] = noscroll ? current_scroll : { x: 0, y: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
cid = id;
|
|
||||||
|
|
||||||
if (root_component) {
|
|
||||||
root_component.set({ preloading: true });
|
|
||||||
}
|
|
||||||
const loaded = prefetching && prefetching.href === target.url.href ?
|
|
||||||
prefetching.promise :
|
|
||||||
prepare_page(target);
|
|
||||||
|
|
||||||
prefetching = null;
|
|
||||||
|
|
||||||
const token = current_token = {};
|
|
||||||
|
|
||||||
return loaded.then(({ redirect, data, nullable_depth }) => {
|
|
||||||
if (redirect) {
|
|
||||||
return goto(redirect.location, { replaceState: true });
|
|
||||||
}
|
|
||||||
render(data, nullable_depth, scroll_history[id], noscroll, hash, token);
|
|
||||||
if (document.activeElement) document.activeElement.blur();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function render(data: any, nullable_depth: number, scroll: ScrollPosition, noscroll: boolean, hash: string, token: {}) {
|
|
||||||
if (current_token !== token) return;
|
|
||||||
|
|
||||||
if (root_component) {
|
|
||||||
// first, clear out highest-level root component
|
|
||||||
let level = data.child;
|
|
||||||
for (let i = 0; i < nullable_depth; i += 1) {
|
|
||||||
if (i === nullable_depth) break;
|
|
||||||
level = level.props.child;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { component } = level;
|
|
||||||
level.component = null;
|
|
||||||
root_component.set({ child: data.child });
|
|
||||||
|
|
||||||
// then render new stuff
|
|
||||||
level.component = component;
|
|
||||||
root_component.set(data);
|
|
||||||
} else {
|
|
||||||
// first load — remove SSR'd <head> contents
|
|
||||||
const start = document.querySelector('#sapper-head-start');
|
|
||||||
const end = document.querySelector('#sapper-head-end');
|
|
||||||
|
|
||||||
if (start && end) {
|
|
||||||
while (start.nextSibling !== end) detach(start.nextSibling);
|
|
||||||
detach(start);
|
|
||||||
detach(end);
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.assign(data, root_data);
|
|
||||||
|
|
||||||
root_component = new RootComponent({
|
|
||||||
target,
|
|
||||||
data,
|
|
||||||
store,
|
|
||||||
hydrate: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!noscroll) {
|
|
||||||
if (hash) {
|
|
||||||
// scroll is an element id (from a hash), we need to compute y.
|
|
||||||
const deep_linked = document.querySelector(hash);
|
|
||||||
if (deep_linked) {
|
|
||||||
scroll = {
|
|
||||||
x: 0,
|
|
||||||
y: deep_linked.getBoundingClientRect().top
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scroll_history[cid] = scroll;
|
|
||||||
if (scroll) scrollTo(scroll.x, scroll.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.assign(root_props, data);
|
|
||||||
ready = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function prepare_page(target: Target): Promise<{
|
|
||||||
redirect?: Redirect;
|
|
||||||
data?: any;
|
|
||||||
nullable_depth?: number;
|
|
||||||
}> {
|
|
||||||
const { page, path, query } = target;
|
|
||||||
const new_segments = path.split('/').filter(Boolean);
|
|
||||||
let changed_from = 0;
|
|
||||||
|
|
||||||
while (
|
|
||||||
segments[changed_from] &&
|
|
||||||
new_segments[changed_from] &&
|
|
||||||
segments[changed_from] === new_segments[changed_from]
|
|
||||||
) changed_from += 1;
|
|
||||||
|
|
||||||
let redirect: Redirect = null;
|
|
||||||
let error: { statusCode: number, message: Error | string } = null;
|
|
||||||
|
|
||||||
const preload_context = {
|
|
||||||
store,
|
|
||||||
fetch: (url: string, opts?: any) => fetch(url, opts),
|
|
||||||
redirect: (statusCode: number, location: string) => {
|
|
||||||
if (redirect && (redirect.statusCode !== statusCode || redirect.location !== location)) {
|
|
||||||
throw new Error(`Conflicting redirects`);
|
|
||||||
}
|
|
||||||
redirect = { statusCode, location };
|
|
||||||
},
|
|
||||||
error: (statusCode: number, message: Error | string) => {
|
|
||||||
error = { statusCode, message };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!root_preload) {
|
|
||||||
root_preload = RootComponent.preload
|
|
||||||
? initial_data.preloaded[0] || RootComponent.preload.call(preload_context, {
|
|
||||||
path,
|
|
||||||
query,
|
|
||||||
params: {}
|
|
||||||
})
|
|
||||||
: {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.all(page.parts.map((part, i) => {
|
|
||||||
if (i < changed_from) return null;
|
|
||||||
if (!part) return null;
|
|
||||||
|
|
||||||
return load_component(components[part.i]).then(Component => {
|
|
||||||
const req = {
|
|
||||||
path,
|
|
||||||
query,
|
|
||||||
params: part.params ? part.params(target.match) : {}
|
|
||||||
};
|
|
||||||
|
|
||||||
let preloaded;
|
|
||||||
if (ready || !initial_data.preloaded[i + 1]) {
|
|
||||||
preloaded = Component.preload
|
|
||||||
? Component.preload.call(preload_context, req)
|
|
||||||
: {};
|
|
||||||
} else {
|
|
||||||
preloaded = initial_data.preloaded[i + 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve(preloaded).then(preloaded => {
|
|
||||||
return { Component, preloaded };
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})).catch(err => {
|
|
||||||
error = { statusCode: 500, message: err };
|
|
||||||
return [];
|
|
||||||
}).then(results => {
|
|
||||||
if (root_data) {
|
|
||||||
return results;
|
|
||||||
} else {
|
|
||||||
return Promise.resolve(root_preload).then(value => {
|
|
||||||
root_data = value;
|
|
||||||
return results;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).then(results => {
|
|
||||||
if (redirect) {
|
|
||||||
return { redirect };
|
|
||||||
}
|
|
||||||
|
|
||||||
segments = new_segments;
|
|
||||||
|
|
||||||
const get_params = page.parts[page.parts.length - 1].params || (() => ({}));
|
|
||||||
const params = get_params(target.match);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
const props = {
|
|
||||||
path,
|
|
||||||
query,
|
|
||||||
params,
|
|
||||||
error: typeof error.message === 'string' ? new Error(error.message) : error.message,
|
|
||||||
status: error.statusCode
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: Object.assign({}, props, {
|
|
||||||
preloading: false,
|
|
||||||
child: {
|
|
||||||
component: ErrorComponent,
|
|
||||||
props
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = { path, query, error: null, status: null };
|
|
||||||
const data = {
|
|
||||||
path,
|
|
||||||
preloading: false,
|
|
||||||
child: Object.assign({}, root_props.child, {
|
|
||||||
segment: segments[0]
|
|
||||||
})
|
|
||||||
};
|
|
||||||
if (changed(query, root_props.query)) data.query = query;
|
|
||||||
if (changed(params, root_props.params)) data.params = params;
|
|
||||||
|
|
||||||
let level = data.child;
|
|
||||||
let nullable_depth = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < page.parts.length; i += 1) {
|
|
||||||
const part = page.parts[i];
|
|
||||||
if (!part) continue;
|
|
||||||
|
|
||||||
const get_params = part.params || (() => ({}));
|
|
||||||
|
|
||||||
if (i < changed_from) {
|
|
||||||
level.props.path = path;
|
|
||||||
level.props.query = query;
|
|
||||||
level.props.child = Object.assign({}, level.props.child);
|
|
||||||
|
|
||||||
nullable_depth += 1;
|
|
||||||
} else {
|
|
||||||
level.component = results[i].Component;
|
|
||||||
level.props = Object.assign({}, level.props, props, {
|
|
||||||
params: get_params(target.match),
|
|
||||||
}, results[i].preloaded);
|
|
||||||
|
|
||||||
level.props.child = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
level = level.props.child;
|
|
||||||
level.segment = segments[i + 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
return { data, nullable_depth };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function load_css(chunk: string) {
|
|
||||||
const href = `client/${chunk}`;
|
|
||||||
if (document.querySelector(`link[href="${href}"]`)) return;
|
|
||||||
|
|
||||||
return new Promise((fulfil, reject) => {
|
|
||||||
const link = document.createElement('link');
|
|
||||||
link.rel = 'stylesheet';
|
|
||||||
link.href = href;
|
|
||||||
|
|
||||||
link.onload = () => fulfil();
|
|
||||||
link.onerror = reject;
|
|
||||||
|
|
||||||
document.head.appendChild(link);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function load_component(component: ComponentLoader): Promise<ComponentConstructor> {
|
|
||||||
// TODO this is temporary — once placeholders are
|
|
||||||
// always rewritten, scratch the ternary
|
|
||||||
const promises: Array<Promise<any>> = (typeof component.css === 'string' ? [] : component.css.map(load_css));
|
|
||||||
promises.unshift(component.js());
|
|
||||||
return Promise.all(promises).then(values => values[0].default);
|
|
||||||
}
|
|
||||||
|
|
||||||
function detach(node: Node) {
|
|
||||||
node.parentNode.removeChild(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
function changed(a: Record<string, string | true>, b: Record<string, string | true>) {
|
|
||||||
return JSON.stringify(a) !== JSON.stringify(b);
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export { default as start } from './start/index';
|
|
||||||
export { default as goto } from './goto/index';
|
|
||||||
export { default as prefetch } from './prefetch/index';
|
|
||||||
export { default as prefetchRoutes } from './prefetchRoutes/index';
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { select_route, prefetching, set_prefetching, prepare_page } from '../app';
|
|
||||||
import { Target } from '../types';
|
|
||||||
|
|
||||||
export default function prefetch(href: string) {
|
|
||||||
const target: Target = select_route(new URL(href, document.baseURI));
|
|
||||||
|
|
||||||
if (target) {
|
|
||||||
if (!prefetching || href !== prefetching.href) {
|
|
||||||
set_prefetching(href, prepare_page(target));
|
|
||||||
}
|
|
||||||
|
|
||||||
return prefetching.promise;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Manifest } from './types';
|
|
||||||
|
|
||||||
export const manifest: Manifest = __MANIFEST__;
|
|
||||||
|
|
||||||
export const build_dir = __BUILD__DIR__;
|
|
||||||
|
|
||||||
export const src_dir = __SRC__DIR__;
|
|
||||||
|
|
||||||
export const dev = __DEV__;
|
|
||||||
|
|
||||||
export const IGNORE = '__SAPPER__IGNORE__';
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as puppeteer from 'puppeteer';
|
import puppeteer from 'puppeteer';
|
||||||
import * as ports from 'port-authority';
|
import * as ports from 'port-authority';
|
||||||
import { fork, ChildProcess } from 'child_process';
|
import { fork, ChildProcess } from 'child_process';
|
||||||
|
|
||||||
@@ -64,11 +64,11 @@ export class AppRunner {
|
|||||||
base: `http://localhost:${this.port}`,
|
base: `http://localhost:${this.port}`,
|
||||||
|
|
||||||
// helpers
|
// helpers
|
||||||
start: () => this.page.evaluate(() => start()),
|
start: () => this.page.evaluate(() => start()).then(() => void 0),
|
||||||
prefetchRoutes: () => this.page.evaluate(() => prefetchRoutes()),
|
prefetchRoutes: () => this.page.evaluate(() => prefetchRoutes()).then(() => void 0),
|
||||||
prefetch: (href: string) => this.page.evaluate((href: string) => prefetch(href), href),
|
prefetch: (href: string) => this.page.evaluate((href: string) => prefetch(href), href).then(() => void 0),
|
||||||
goto: (href: string) => this.page.evaluate((href: string) => goto(href), href),
|
goto: (href: string) => this.page.evaluate((href: string) => goto(href), href).then(() => void 0),
|
||||||
title: () => this.page.$eval('h1', node => node.textContent)
|
title: () => this.page.$eval('h1', node => node.textContent).then(serializable => String(serializable))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,10 +22,7 @@ export default {
|
|||||||
emitCss: true
|
emitCss: true
|
||||||
}),
|
}),
|
||||||
resolve()
|
resolve()
|
||||||
],
|
]
|
||||||
|
|
||||||
// temporary, pending Rollup 1.0
|
|
||||||
experimentalCodeSplitting: true
|
|
||||||
},
|
},
|
||||||
|
|
||||||
server: {
|
server: {
|
||||||
@@ -44,10 +41,7 @@ export default {
|
|||||||
preferBuiltins: true
|
preferBuiltins: true
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
external: ['sirv', 'polka'],
|
external: ['sirv', 'polka']
|
||||||
|
|
||||||
// temporary, pending Rollup 1.0
|
|
||||||
experimentalCodeSplitting: true
|
|
||||||
},
|
},
|
||||||
|
|
||||||
serviceworker: {
|
serviceworker: {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as sapper from '../__sapper__/client.js';
|
import * as sapper from '@sapper/app';
|
||||||
|
|
||||||
window.start = () => sapper.start({
|
window.start = () => sapper.start({
|
||||||
target: document.querySelector('#sapper')
|
target: document.querySelector('#sapper')
|
||||||
|
|||||||
5
test/apps/basics/src/routes/[slug].svelte
Normal file
5
test/apps/basics/src/routes/[slug].svelte
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<script>
|
||||||
|
import { page } from '@sapper/app';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1>{$page.params.slug.toUpperCase()}</h1>
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<h1>{letter}</h1>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
preload() {
|
|
||||||
return this.fetch('b.json').then(r => r.json()).then(letter => {
|
|
||||||
return { letter };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
13
test/apps/basics/src/routes/b/index.svelte
Normal file
13
test/apps/basics/src/routes/b/index.svelte
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<script context="module">
|
||||||
|
export function preload() {
|
||||||
|
return this.fetch('b.json').then(r => r.json()).then(letter => {
|
||||||
|
return { letter };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export let letter;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1>{letter}</h1>
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<button class='del' on:click='del()'>delete</button>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
oncreate() {
|
|
||||||
window.deleted = null;
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
del() {
|
|
||||||
fetch(`delete-test/42.json`, { method: 'DELETE' })
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(data => {
|
|
||||||
window.deleted = data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
17
test/apps/basics/src/routes/delete-test/index.svelte
Normal file
17
test/apps/basics/src/routes/delete-test/index.svelte
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
window.deleted = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
function del() {
|
||||||
|
fetch(`delete-test/42.json`, { method: 'DELETE' })
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
window.deleted = data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button class="del" on:click={del}>delete</button>
|
||||||
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>
|
||||||
@@ -1 +0,0 @@
|
|||||||
<h1>{JSON.stringify(query)}</h1>
|
|
||||||
5
test/apps/basics/src/routes/echo-query/index.svelte
Normal file
5
test/apps/basics/src/routes/echo-query/index.svelte
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<script>
|
||||||
|
import { page } from '@sapper/app';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1>{JSON.stringify($page.query)}</h1>
|
||||||
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>
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
$&
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
preload() {
|
|
||||||
return '$&';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
7
test/apps/basics/src/routes/unsafe-replacement.svelte
Normal file
7
test/apps/basics/src/routes/unsafe-replacement.svelte
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<script context="module">
|
||||||
|
export function preload() {
|
||||||
|
return '$&';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
$&
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import polka from 'polka';
|
import polka from 'polka';
|
||||||
import * as sapper from '../__sapper__/server.js';
|
import * as sapper from '@sapper/server';
|
||||||
|
|
||||||
const { PORT } = process.env;
|
const { PORT } = process.env;
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { timestamp, files, shell, routes } from '../__sapper__/service-worker.js';
|
import * as sapper from '@sapper/service-worker';
|
||||||
|
|
||||||
const ASSETS = `cache${timestamp}`;
|
const ASSETS = `cache${sapper.timestamp}`;
|
||||||
|
|
||||||
// `shell` is an array of all the files generated by webpack,
|
// `app.shell` is an array of all the files generated by webpack,
|
||||||
// `files` is an array of everything in the `static` directory
|
// `app.files` is an array of everything in the `static` directory
|
||||||
const to_cache = shell.concat(ASSETS);
|
const to_cache = sapper.shell.concat(sapper.files);
|
||||||
const cached = new Set(to_cache);
|
const cached = new Set(to_cache);
|
||||||
|
|
||||||
self.addEventListener('install', event => {
|
self.addEventListener('install', event => {
|
||||||
@@ -52,7 +52,6 @@ self.addEventListener('fetch', event => {
|
|||||||
// which Sapper has generated for you. It's not right for every
|
// which Sapper has generated for you. It's not right for every
|
||||||
// app, but if it's right for yours then uncomment this section
|
// 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'));
|
event.respondWith(caches.match('/index.html'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -65,7 +64,7 @@ self.addEventListener('fetch', event => {
|
|||||||
// might prefer a cache-first approach to a network-first one.)
|
// might prefer a cache-first approach to a network-first one.)
|
||||||
event.respondWith(
|
event.respondWith(
|
||||||
caches
|
caches
|
||||||
.open(`offline${timestamp}`)
|
.open(`offline${sapper.timestamp}`)
|
||||||
.then(async cache => {
|
.then(async cache => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(event.request);
|
const response = await fetch(event.request);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
@@ -20,19 +39,18 @@ describe('basics', function() {
|
|||||||
let prefetchRoutes: () => Promise<void>;
|
let prefetchRoutes: () => Promise<void>;
|
||||||
let prefetch: (href: string) => Promise<void>;
|
let prefetch: (href: string) => Promise<void>;
|
||||||
let goto: (href: string) => Promise<void>;
|
let goto: (href: string) => Promise<void>;
|
||||||
|
let title: () => Promise<string>;
|
||||||
|
|
||||||
// hooks
|
// hooks
|
||||||
before(async () => {
|
before(async () => {
|
||||||
await build({ cwd: __dirname });
|
await build({ cwd: __dirname });
|
||||||
|
|
||||||
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
|
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
|
||||||
({ base, page, start, prefetchRoutes, prefetch, goto } = await runner.start());
|
({ base, page, start, prefetchRoutes, prefetch, goto, title } = await runner.start());
|
||||||
});
|
});
|
||||||
|
|
||||||
after(() => runner.end());
|
after(() => runner.end());
|
||||||
|
|
||||||
const title = () => page.$eval('h1', node => node.textContent);
|
|
||||||
|
|
||||||
it('serves /', async () => {
|
it('serves /', async () => {
|
||||||
await page.goto(base);
|
await page.goto(base);
|
||||||
|
|
||||||
@@ -117,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 () => {
|
||||||
@@ -262,7 +267,43 @@ describe('basics', function() {
|
|||||||
await page.goto(`${base}/unsafe-replacement`);
|
await page.goto(`${base}/unsafe-replacement`);
|
||||||
await start();
|
await start();
|
||||||
|
|
||||||
const html = await page.evaluate(() => document.body.innerHTML);
|
const html = String(await page.evaluate(() => document.body.innerHTML));
|
||||||
assert.equal(html.indexOf('%sapper'), -1);
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -22,10 +22,7 @@ export default {
|
|||||||
emitCss: true
|
emitCss: true
|
||||||
}),
|
}),
|
||||||
resolve()
|
resolve()
|
||||||
],
|
]
|
||||||
|
|
||||||
// temporary, pending Rollup 1.0
|
|
||||||
experimentalCodeSplitting: true
|
|
||||||
},
|
},
|
||||||
|
|
||||||
server: {
|
server: {
|
||||||
@@ -44,10 +41,7 @@ export default {
|
|||||||
preferBuiltins: true
|
preferBuiltins: true
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
external: ['sirv', 'polka', 'cookie'],
|
external: ['sirv', 'polka', 'cookie']
|
||||||
|
|
||||||
// temporary, pending Rollup 1.0
|
|
||||||
experimentalCodeSplitting: true
|
|
||||||
},
|
},
|
||||||
|
|
||||||
serviceworker: {
|
serviceworker: {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as sapper from '../__sapper__/client.js';
|
import * as sapper from '@sapper/app';
|
||||||
|
|
||||||
window.start = () => sapper.start({
|
window.start = () => sapper.start({
|
||||||
target: document.querySelector('#sapper')
|
target: document.querySelector('#sapper')
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
<h1>{message}</h1>
|
<script context="module">
|
||||||
|
export function preload({ query }) {
|
||||||
|
return this.fetch(`credentials/test.json`, {
|
||||||
|
credentials: query.creds
|
||||||
|
}).then(r => r.json());
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export let message;
|
||||||
preload({ query }) {
|
|
||||||
return this.fetch(`credentials/test.json`, {
|
|
||||||
credentials: query.creds
|
|
||||||
}).then(r => r.json());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<h1>{message}</h1>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import polka from 'polka';
|
import polka from 'polka';
|
||||||
import * as sapper from '../__sapper__/server.js';
|
import * as sapper from '@sapper/server';
|
||||||
|
|
||||||
const { PORT } = process.env;
|
const { PORT } = process.env;
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { timestamp, files, shell, routes } from '../__sapper__/service-worker.js';
|
import * as sapper from '@sapper/service-worker';
|
||||||
|
|
||||||
const ASSETS = `cache${timestamp}`;
|
const ASSETS = `cache${sapper.timestamp}`;
|
||||||
|
|
||||||
// `shell` is an array of all the files generated by webpack,
|
// `shell` is an array of all the files generated by webpack,
|
||||||
// `files` is an array of everything in the `static` directory
|
// `files` is an array of everything in the `static` directory
|
||||||
const to_cache = shell.concat(ASSETS);
|
const to_cache = sapper.shell.concat(sapper.files);
|
||||||
const cached = new Set(to_cache);
|
const cached = new Set(to_cache);
|
||||||
|
|
||||||
self.addEventListener('install', event => {
|
self.addEventListener('install', event => {
|
||||||
@@ -65,7 +65,7 @@ self.addEventListener('fetch', event => {
|
|||||||
// might prefer a cache-first approach to a network-first one.)
|
// might prefer a cache-first approach to a network-first one.)
|
||||||
event.respondWith(
|
event.respondWith(
|
||||||
caches
|
caches
|
||||||
.open(`offline${timestamp}`)
|
.open(`offline${sapper.timestamp}`)
|
||||||
.then(async cache => {
|
.then(async cache => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(event.request);
|
const response = await fetch(event.request);
|
||||||
|
|||||||
@@ -22,10 +22,7 @@ export default {
|
|||||||
emitCss: true
|
emitCss: true
|
||||||
}),
|
}),
|
||||||
resolve()
|
resolve()
|
||||||
],
|
]
|
||||||
|
|
||||||
// temporary, pending Rollup 1.0
|
|
||||||
experimentalCodeSplitting: true
|
|
||||||
},
|
},
|
||||||
|
|
||||||
server: {
|
server: {
|
||||||
@@ -44,10 +41,7 @@ export default {
|
|||||||
preferBuiltins: true
|
preferBuiltins: true
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
external: ['sirv', 'polka'],
|
external: ['sirv', 'polka']
|
||||||
|
|
||||||
// temporary, pending Rollup 1.0
|
|
||||||
experimentalCodeSplitting: true
|
|
||||||
},
|
},
|
||||||
|
|
||||||
serviceworker: {
|
serviceworker: {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as sapper from '../__sapper__/client.js';
|
import * as sapper from '@sapper/app';
|
||||||
|
|
||||||
window.start = () => sapper.start({
|
window.start = () => sapper.start({
|
||||||
target: document.querySelector('#sapper')
|
target: document.querySelector('#sapper')
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
<svelte:component this={Title}/>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
oncreate() {
|
|
||||||
import('./_components/Title.html').then(({ default: Title }) => {
|
|
||||||
this.set({ Title });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
13
test/apps/css/src/routes/bar.svelte
Normal file
13
test/apps/css/src/routes/bar.svelte
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
export let Title;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
import('./_components/Title.svelte').then(mod => {
|
||||||
|
Title = mod.default;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:component this={Title}/>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import polka from 'polka';
|
import polka from 'polka';
|
||||||
import * as sapper from '../__sapper__/server.js';
|
import * as sapper from '@sapper/server';
|
||||||
|
|
||||||
const { PORT } = process.env;
|
const { PORT } = process.env;
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { timestamp, files, shell, routes } from '../__sapper__/service-worker.js';
|
import * as sapper from '@sapper/service-worker';
|
||||||
|
|
||||||
const ASSETS = `cache${timestamp}`;
|
const ASSETS = `cache${sapper.timestamp}`;
|
||||||
|
|
||||||
// `shell` is an array of all the files generated by webpack,
|
// `shell` is an array of all the files generated by webpack,
|
||||||
// `files` is an array of everything in the `static` directory
|
// `files` is an array of everything in the `static` directory
|
||||||
const to_cache = shell.concat(ASSETS);
|
const to_cache = sapper.shell.concat(sapper.files);
|
||||||
const cached = new Set(to_cache);
|
const cached = new Set(to_cache);
|
||||||
|
|
||||||
self.addEventListener('install', event => {
|
self.addEventListener('install', event => {
|
||||||
@@ -65,7 +65,7 @@ self.addEventListener('fetch', event => {
|
|||||||
// might prefer a cache-first approach to a network-first one.)
|
// might prefer a cache-first approach to a network-first one.)
|
||||||
event.respondWith(
|
event.respondWith(
|
||||||
caches
|
caches
|
||||||
.open(`offline${timestamp}`)
|
.open(`offline${sapper.timestamp}`)
|
||||||
.then(async cache => {
|
.then(async cache => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(event.request);
|
const response = await fetch(event.request);
|
||||||
|
|||||||
@@ -22,10 +22,7 @@ export default {
|
|||||||
emitCss: true
|
emitCss: true
|
||||||
}),
|
}),
|
||||||
resolve()
|
resolve()
|
||||||
],
|
]
|
||||||
|
|
||||||
// temporary, pending Rollup 1.0
|
|
||||||
experimentalCodeSplitting: true
|
|
||||||
},
|
},
|
||||||
|
|
||||||
server: {
|
server: {
|
||||||
@@ -44,10 +41,7 @@ export default {
|
|||||||
preferBuiltins: true
|
preferBuiltins: true
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
external: ['sirv', 'polka'],
|
external: ['sirv', 'polka']
|
||||||
|
|
||||||
// temporary, pending Rollup 1.0
|
|
||||||
experimentalCodeSplitting: true
|
|
||||||
},
|
},
|
||||||
|
|
||||||
serviceworker: {
|
serviceworker: {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as sapper from '../__sapper__/client.js';
|
import * as sapper from '@sapper/app';
|
||||||
|
|
||||||
window.start = () => sapper.start({
|
window.start = () => sapper.start({
|
||||||
target: document.querySelector('#sapper')
|
target: document.querySelector('#sapper')
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
<h1>{slug} {JSON.stringify(query)}</h1>
|
<script context="module">
|
||||||
|
export function preload({ params }) {
|
||||||
|
return {
|
||||||
|
slug: params.slug
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
import { page } from '@sapper/app';
|
||||||
preload({ params }) {
|
export let slug;
|
||||||
return {
|
|
||||||
slug: params.slug
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<h1>{slug} {JSON.stringify($page.query)}</h1>
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
<h1>{phrase}</h1>
|
<script context="module">
|
||||||
|
export function preload() {
|
||||||
|
return this.fetch('fünke.json').then(r => r.json()).then(phrase => {
|
||||||
|
return { phrase };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export let phrase;
|
||||||
preload() {
|
|
||||||
return this.fetch('fünke.json').then(r => r.json()).then(phrase => {
|
|
||||||
return { phrase };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<h1>{phrase}</h1>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import polka from 'polka';
|
import polka from 'polka';
|
||||||
import * as sapper from '../__sapper__/server.js';
|
import * as sapper from '@sapper/server';
|
||||||
|
|
||||||
const { PORT } = process.env;
|
const { PORT } = process.env;
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { timestamp, files, shell, routes } from '../__sapper__/service-worker.js';
|
import * as sapper from '@sapper/service-worker';
|
||||||
|
|
||||||
const ASSETS = `cache${timestamp}`;
|
const ASSETS = `cache${sapper.timestamp}`;
|
||||||
|
|
||||||
// `shell` is an array of all the files generated by webpack,
|
// `shell` is an array of all the files generated by webpack,
|
||||||
// `files` is an array of everything in the `static` directory
|
// `files` is an array of everything in the `static` directory
|
||||||
const to_cache = shell.concat(ASSETS);
|
const to_cache = sapper.shell.concat(sapper.files);
|
||||||
const cached = new Set(to_cache);
|
const cached = new Set(to_cache);
|
||||||
|
|
||||||
self.addEventListener('install', event => {
|
self.addEventListener('install', event => {
|
||||||
@@ -65,7 +65,7 @@ self.addEventListener('fetch', event => {
|
|||||||
// might prefer a cache-first approach to a network-first one.)
|
// might prefer a cache-first approach to a network-first one.)
|
||||||
event.respondWith(
|
event.respondWith(
|
||||||
caches
|
caches
|
||||||
.open(`offline${timestamp}`)
|
.open(`offline${sapper.timestamp}`)
|
||||||
.then(async cache => {
|
.then(async cache => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(event.request);
|
const response = await fetch(event.request);
|
||||||
|
|||||||
@@ -22,10 +22,7 @@ export default {
|
|||||||
emitCss: true
|
emitCss: true
|
||||||
}),
|
}),
|
||||||
resolve()
|
resolve()
|
||||||
],
|
]
|
||||||
|
|
||||||
// temporary, pending Rollup 1.0
|
|
||||||
experimentalCodeSplitting: true
|
|
||||||
},
|
},
|
||||||
|
|
||||||
server: {
|
server: {
|
||||||
@@ -44,10 +41,7 @@ export default {
|
|||||||
preferBuiltins: true
|
preferBuiltins: true
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
external: ['sirv', 'polka'],
|
external: ['sirv', 'polka']
|
||||||
|
|
||||||
// temporary, pending Rollup 1.0
|
|
||||||
experimentalCodeSplitting: true
|
|
||||||
},
|
},
|
||||||
|
|
||||||
serviceworker: {
|
serviceworker: {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as sapper from '../__sapper__/client.js';
|
import * as sapper from '@sapper/app';
|
||||||
|
|
||||||
window.start = () => sapper.start({
|
window.start = () => sapper.start({
|
||||||
target: document.querySelector('#sapper')
|
target: document.querySelector('#sapper')
|
||||||
|
|||||||
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>
|
||||||
@@ -1,19 +1,21 @@
|
|||||||
<h1>{post.title}</h1>
|
<script context="module">
|
||||||
|
export function preload({ params }) {
|
||||||
|
const { slug } = params;
|
||||||
|
|
||||||
|
return this.fetch(`blog/${slug}.json`).then(r => {
|
||||||
|
return r.json().then(data => {
|
||||||
|
if (r.status !== 200) {
|
||||||
|
this.error(r.status, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export let post;
|
||||||
preload({ params }) {
|
|
||||||
const { slug } = params;
|
|
||||||
|
|
||||||
return this.fetch(`blog/${slug}.json`).then(r => {
|
|
||||||
return r.json().then(data => {
|
|
||||||
if (r.status !== 200) {
|
|
||||||
this.error(r.status, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<h1>{post.title}</h1>
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<script>
|
|
||||||
export default {
|
|
||||||
preload() {
|
|
||||||
this.error(420, 'Enhance your calm');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
5
test/apps/errors/src/routes/enhance-your-calm.svelte
Normal file
5
test/apps/errors/src/routes/enhance-your-calm.svelte
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<script context="module">
|
||||||
|
export function preload() {
|
||||||
|
this.error(420, 'Enhance your calm');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user