mirror of
https://github.com/kevin-DL/sapper.git
synced 2026-01-13 19:45:26 +00:00
Compare commits
198 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 | ||
|
|
969169ae20 | ||
|
|
79fa15da3d | ||
|
|
ddc08d94cc | ||
|
|
f587161d7d | ||
|
|
d486542a8b | ||
|
|
c990c771d8 | ||
|
|
90f3393ebf | ||
|
|
76ce7f227f | ||
|
|
1f9efd353c | ||
|
|
3499631e8e | ||
|
|
2a825269e9 | ||
|
|
83f7102f6b | ||
|
|
e4319bee0e | ||
|
|
da540ef15f | ||
|
|
c00af6dad0 | ||
|
|
92206742d4 | ||
|
|
14fc6b3176 | ||
|
|
e2193a6080 | ||
|
|
f66c7dcb0d | ||
|
|
06f1a0e6c0 | ||
|
|
7726325b4b | ||
|
|
b6bc90cea9 | ||
|
|
cfba9b2168 | ||
|
|
b25c642bf1 | ||
|
|
82a023c302 | ||
|
|
f97400caaa | ||
|
|
03af9b1a16 | ||
|
|
02cef046aa | ||
|
|
c2aeac34b6 | ||
|
|
abd2f7fd39 | ||
|
|
e7cf9bf1b6 | ||
|
|
1fdd0e3ad2 | ||
|
|
af0a7e04f9 | ||
|
|
ed19a19fed | ||
|
|
8ebfcc9a54 | ||
|
|
af2a792508 | ||
|
|
14e809af6e | ||
|
|
03c5f5b446 | ||
|
|
6655b1b49d | ||
|
|
eebf076f23 | ||
|
|
198be28f4b | ||
|
|
4f720446b2 | ||
|
|
e69cb3639a | ||
|
|
1b1a86764f | ||
|
|
f50f83c4a4 | ||
|
|
eadefd996b | ||
|
|
ab52aabd1d | ||
|
|
c5a80543b3 | ||
|
|
cfd95ac024 | ||
|
|
73ff95c677 | ||
|
|
382fe6b7b9 | ||
|
|
3b714c0de3 | ||
|
|
28186227a9 | ||
|
|
2ac0f2bf3d | ||
|
|
4991f3b359 | ||
|
|
65128118c7 | ||
|
|
3eced6fa4d | ||
|
|
c4aee66c32 | ||
|
|
410c52df41 | ||
|
|
ffd56e2a20 | ||
|
|
1e5a87cf71 | ||
|
|
281e183c61 | ||
|
|
3fe7b55955 | ||
|
|
464924ed67 | ||
|
|
8108642845 | ||
|
|
e5d7d8ab2b | ||
|
|
d3e560325d | ||
|
|
64e5065aa5 | ||
|
|
cb45bb0fbe | ||
|
|
f39455014a | ||
|
|
4fe8df3696 | ||
|
|
4fdc7055c1 | ||
|
|
cca417a85a | ||
|
|
635c13a175 | ||
|
|
2e3aef8b21 | ||
|
|
44736754ad | ||
|
|
a399d87d9b | ||
|
|
a68c62ce91 | ||
|
|
1b9b559d82 | ||
|
|
abcac75826 | ||
|
|
4118c566d1 | ||
|
|
0b76f12394 | ||
|
|
e51cb85c7c | ||
|
|
6ae9a5e7c5 | ||
|
|
52f40f9e63 | ||
|
|
5e59855a15 | ||
|
|
18acef3190 | ||
|
|
d7f6ca8b4d | ||
|
|
00321932ef | ||
|
|
7eb1ec727c | ||
|
|
3f586e19a1 | ||
|
|
05b702938f | ||
|
|
3026e7c36e | ||
|
|
27a5aed83e | ||
|
|
bb04af41bd | ||
|
|
9403799393 | ||
|
|
472c0c198a | ||
|
|
02256ae214 | ||
|
|
e2d325ec9f | ||
|
|
954bcba333 | ||
|
|
709c9992e3 | ||
|
|
18e6f29de7 | ||
|
|
d0c6b9cdca |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -12,4 +12,5 @@ sapper
|
||||
runtime.js
|
||||
dist
|
||||
!rollup.config.js
|
||||
templates/*.js
|
||||
/runtime/app.mjs
|
||||
/runtime/server.mjs
|
||||
@@ -3,7 +3,6 @@ sudo: false
|
||||
language: node_js
|
||||
|
||||
node_js:
|
||||
- "6"
|
||||
- "stable"
|
||||
|
||||
env:
|
||||
|
||||
77
CHANGELOG.md
77
CHANGELOG.md
@@ -1,5 +1,82 @@
|
||||
# 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
|
||||
|
||||
* Add service-worker-index.html shell file for offline support ([#422](https://github.com/sveltejs/sapper/issues/422))
|
||||
* Don't cache .map files ([#534](https://github.com/sveltejs/sapper/issues/534))
|
||||
|
||||
## 0.24.2
|
||||
|
||||
* Support Rollup 1.0 ([#541](https://github.com/sveltejs/sapper/pull/541))
|
||||
|
||||
## 0.24.1
|
||||
|
||||
* Include CSS chunks in webpack build info to avoid duplication ([#529](https://github.com/sveltejs/sapper/pull/529))
|
||||
* Fix preload `as` for styles ([#530](https://github.com/sveltejs/sapper/pull/530))
|
||||
|
||||
## 0.24.0
|
||||
|
||||
* Handle external URLs in `this.redirect` ([#490](https://github.com/sveltejs/sapper/issues/490))
|
||||
* Strip leading `/` from basepath ([#495](https://github.com/sveltejs/sapper/issues/495))
|
||||
* Treat duplicate query string parameters as arrays ([#497](https://github.com/sveltejs/sapper/issues/497))
|
||||
* Don't buffer `stdout` and `stderr` ([#305](https://github.com/sveltejs/sapper/issues/305))
|
||||
* Posixify `build_dir` ([#498](https://github.com/sveltejs/sapper/pull/498))
|
||||
* Use `page[XY]Offset` instead of `scroll[XY]` ([#480](https://github.com/sveltejs/sapper/issues/480))
|
||||
|
||||
## 0.23.5
|
||||
|
||||
* Include lazily-imported CSS in main CSS chunk ([#492](https://github.com/sveltejs/sapper/pull/492))
|
||||
* Make search param decoding spec-compliant ([#493](https://github.com/sveltejs/sapper/pull/493))
|
||||
* Handle async route errors ([#488](https://github.com/sveltejs/sapper/pull/488))
|
||||
|
||||
## 0.23.4
|
||||
|
||||
* Ignore empty anchors when exporting ([#491](https://github.com/sveltejs/sapper/pull/491))
|
||||
|
||||
## 0.23.3
|
||||
|
||||
* Clear `error` and `status` on successful render ([#477](https://github.com/sveltejs/sapper/pull/477))
|
||||
|
||||
## 0.23.2
|
||||
|
||||
* Fix entry point CSS ([#471](https://github.com/sveltejs/sapper/pull/471))
|
||||
|
||||
## 0.23.1
|
||||
|
||||
* Scroll to deeplink that matches current URL ([#472](https://github.com/sveltejs/sapper/pull/472))
|
||||
* Scroll to deeplink on another page ([#341](https://github.com/sveltejs/sapper/issues/341))
|
||||
|
||||
## 0.23.0
|
||||
|
||||
* Overhaul internal APIs ([#468](https://github.com/sveltejs/sapper/pull/468))
|
||||
* Remove unused `sapper start` and `sapper upgrade` ([#468](https://github.com/sveltejs/sapper/pull/468))
|
||||
* Remove magic environment variables ([#469](https://github.com/sveltejs/sapper/pull/469))
|
||||
* Preserve SSI comments ([#470](https://github.com/sveltejs/sapper/pull/470))
|
||||
|
||||
## 0.22.10
|
||||
|
||||
* Handle `sapper-noscroll` attribute on `<a>` elements ([#376](https://github.com/sveltejs/sapper/issues/376))
|
||||
* Fix CSS paths when using a base path ([#466](https://github.com/sveltejs/sapper/pull/466))
|
||||
|
||||
## 0.22.9
|
||||
|
||||
* Fix legacy builds ([#462](https://github.com/sveltejs/sapper/pull/462))
|
||||
|
||||
## 0.22.8
|
||||
|
||||
* Ensure CSS placeholders are overwritten ([#462](https://github.com/sveltejs/sapper/pull/462))
|
||||
|
||||
## 0.22.7
|
||||
|
||||
* Fix cookies ([#460](https://github.com/sveltejs/sapper/pull/460))
|
||||
|
||||
## 0.22.6
|
||||
|
||||
* Normalise chunk filenames on Windows ([#456](https://github.com/sveltejs/sapper/pull/456))
|
||||
|
||||
@@ -11,9 +11,11 @@ Sapper is a framework for building high-performance universal web apps. [Read th
|
||||
## Get started
|
||||
|
||||
Clone the [starter project template](https://github.com/sveltejs/sapper-template) with [degit](https://github.com/rich-harris/degit)...
|
||||
When cloning you have to choose between rollup or webpack:
|
||||
|
||||
```bash
|
||||
npx degit sveltejs/sapper-template my-app
|
||||
npx degit "sveltejs/sapper-template#rollup" my-app
|
||||
# or: npx degit "sveltejs/sapper-template#webpack" my-app
|
||||
```
|
||||
|
||||
...then install dependencies and start the dev server...
|
||||
|
||||
@@ -10,7 +10,7 @@ build: off
|
||||
environment:
|
||||
matrix:
|
||||
# node.js
|
||||
- nodejs_version: 10.5
|
||||
- nodejs_version: 11
|
||||
|
||||
install:
|
||||
- ps: Install-Product node $env:nodejs_version
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"baseUrl": "http://localhost:3000",
|
||||
"videoRecording": false,
|
||||
"fixturesFolder": "test/cypress/fixtures",
|
||||
"integrationFolder": "test/cypress/integration",
|
||||
"pluginsFile": false,
|
||||
"screenshotsFolder": "test/cypress/screenshots",
|
||||
"supportFile": "test/cypress/support/index.js"
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
--require source-map-support/register
|
||||
--require ts-node/register
|
||||
--require sucrase/register
|
||||
--recursive
|
||||
test/unit/*/*.ts
|
||||
test/common/test.js
|
||||
test/apps/*/test.ts
|
||||
5382
package-lock.json
generated
5382
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
84
package.json
84
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sapper",
|
||||
"version": "0.22.6",
|
||||
"version": "0.26.0-alpha.12",
|
||||
"description": "Military-grade apps, engineered by Svelte",
|
||||
"bin": {
|
||||
"sapper": "./sapper"
|
||||
@@ -11,72 +11,66 @@
|
||||
"config",
|
||||
"sapper",
|
||||
"dist/*.js",
|
||||
"templates/*.js"
|
||||
"runtime/*.mjs",
|
||||
"runtime/internal"
|
||||
],
|
||||
"directories": {
|
||||
"test": "test"
|
||||
},
|
||||
"dependencies": {
|
||||
"html-minifier": "^3.5.16",
|
||||
"shimport": "0.0.11",
|
||||
"source-map-support": "^0.5.6",
|
||||
"sourcemap-codec": "^1.4.1",
|
||||
"string-hash": "^1.1.3",
|
||||
"tslib": "^1.9.1"
|
||||
"html-minifier": "^3.5.21",
|
||||
"http-link-header": "^1.0.2",
|
||||
"shimport": "0.0.14",
|
||||
"sourcemap-codec": "^1.4.4",
|
||||
"string-hash": "^1.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/glob": "^5.0.34",
|
||||
"@types/mkdirp": "^0.5.2",
|
||||
"@types/mocha": "^5.2.5",
|
||||
"@types/node": "^10.7.1",
|
||||
"@types/rimraf": "^2.0.2",
|
||||
"@types/node": "^10.12.21",
|
||||
"@types/puppeteer": "^1.11.3",
|
||||
"agadoo": "^1.0.1",
|
||||
"cheap-watch": "^0.3.0",
|
||||
"compression": "^1.7.1",
|
||||
"cheap-watch": "^1.0.2",
|
||||
"cookie": "^0.3.1",
|
||||
"devalue": "^1.0.4",
|
||||
"eslint": "^4.13.1",
|
||||
"eslint-plugin-import": "^2.12.0",
|
||||
"express": "^4.16.3",
|
||||
"kleur": "^2.0.1",
|
||||
"mkdirp": "^0.5.1",
|
||||
"devalue": "^1.1.0",
|
||||
"eslint": "^5.12.1",
|
||||
"eslint-plugin-import": "^2.16.0",
|
||||
"kleur": "^3.0.1",
|
||||
"mocha": "^5.2.0",
|
||||
"nightmare": "^3.0.0",
|
||||
"node-fetch": "^2.1.1",
|
||||
"npm-run-all": "^4.1.3",
|
||||
"polka": "^0.4.0",
|
||||
"node-fetch": "^2.3.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"polka": "^0.5.1",
|
||||
"port-authority": "^1.0.5",
|
||||
"pretty-bytes": "^5.0.0",
|
||||
"pretty-ms": "^3.1.0",
|
||||
"pretty-bytes": "^5.1.0",
|
||||
"puppeteer": "^1.12.0",
|
||||
"require-relative": "^0.8.7",
|
||||
"rimraf": "^2.6.2",
|
||||
"rollup": "^0.65.0",
|
||||
"rollup-plugin-commonjs": "^9.1.3",
|
||||
"rollup-plugin-json": "^3.0.0",
|
||||
"rollup-plugin-node-resolve": "^3.3.0",
|
||||
"rollup": "^1.1.2",
|
||||
"rollup-plugin-commonjs": "^9.2.0",
|
||||
"rollup-plugin-json": "^3.1.0",
|
||||
"rollup-plugin-node-resolve": "^4.0.0",
|
||||
"rollup-plugin-replace": "^2.1.0",
|
||||
"rollup-plugin-string": "^2.0.2",
|
||||
"rollup-plugin-typescript": "^0.8.1",
|
||||
"sade": "^1.4.1",
|
||||
"sander": "^0.6.0",
|
||||
"serve-static": "^1.13.2",
|
||||
"svelte": "^2.6.3",
|
||||
"svelte-loader": "^2.9.0",
|
||||
"tiny-glob": "^0.2.2",
|
||||
"ts-node": "^7.0.1",
|
||||
"typescript": "^2.8.3",
|
||||
"walk-sync": "^0.3.2",
|
||||
"webpack": "^4.8.3",
|
||||
"webpack-format-messages": "^2.0.1"
|
||||
"rollup-plugin-sucrase": "^2.1.0",
|
||||
"rollup-plugin-svelte": "^5.0.3",
|
||||
"sade": "^1.4.2",
|
||||
"sirv": "^0.2.2",
|
||||
"sucrase": "^3.9.5",
|
||||
"svelte": "^3.0.0-beta.11",
|
||||
"svelte-loader": "^2.13.3",
|
||||
"webpack": "^4.29.0",
|
||||
"webpack-format-messages": "^2.0.5",
|
||||
"yootils": "0.0.14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"cy:open": "cypress open",
|
||||
"test": "mocha --opts mocha.opts",
|
||||
"pretest": "npm run build",
|
||||
"build": "rm -rf dist && rollup -c",
|
||||
"prepare": "npm run build",
|
||||
"dev": "rollup -cw",
|
||||
"prepublishOnly": "npm test",
|
||||
"update_mime_types": "curl http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types | grep -e \"^[^#]\" > src/middleware/mime-types.md"
|
||||
"update_mime_types": "curl http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types | grep -e \"^[^#]\" > runtime/src/server/middleware/mime-types.md"
|
||||
},
|
||||
"repository": "https://github.com/sveltejs/sapper",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import typescript from 'rollup-plugin-typescript';
|
||||
import sucrase from 'rollup-plugin-sucrase';
|
||||
import string from 'rollup-plugin-string';
|
||||
import json from 'rollup-plugin-json';
|
||||
import resolve from 'rollup-plugin-node-resolve';
|
||||
@@ -9,42 +9,45 @@ import { builtinModules } from 'module';
|
||||
const external = [].concat(
|
||||
Object.keys(pkg.dependencies),
|
||||
Object.keys(process.binding('natives')),
|
||||
'sapper/core.js'
|
||||
'sapper/core.js',
|
||||
'svelte/compiler'
|
||||
);
|
||||
|
||||
function template(kind, external) {
|
||||
return {
|
||||
input: `templates/src/${kind}/index.ts`,
|
||||
input: `runtime/src/${kind}/index.ts`,
|
||||
output: {
|
||||
file: `templates/${kind}.js`,
|
||||
format: 'es'
|
||||
file: `runtime/${kind}.mjs`,
|
||||
format: 'es',
|
||||
paths: id => id.replace('@sapper', '.')
|
||||
},
|
||||
external,
|
||||
plugins: [
|
||||
resolve(),
|
||||
resolve({
|
||||
extensions: ['.mjs', '.js', '.ts']
|
||||
}),
|
||||
commonjs(),
|
||||
string({
|
||||
include: '**/*.md'
|
||||
}),
|
||||
typescript({
|
||||
typescript: require('typescript'),
|
||||
target: "ES2017"
|
||||
sucrase({
|
||||
transforms: ['typescript']
|
||||
})
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
export default [
|
||||
template('client', ['__ROOT__', '__ERROR__']),
|
||||
template('server', builtinModules),
|
||||
template('app', id => /^(svelte\/?|@sapper\/)/.test(id)),
|
||||
template('server', id => /^(svelte\/?|@sapper\/)/.test(id) || builtinModules.includes(id)),
|
||||
|
||||
{
|
||||
input: [
|
||||
`src/api.ts`,
|
||||
`src/cli.ts`,
|
||||
`src/core.ts`,
|
||||
`src/rollup.ts`,
|
||||
`src/webpack.ts`
|
||||
`src/config/rollup.ts`,
|
||||
`src/config/webpack.ts`
|
||||
],
|
||||
output: {
|
||||
dir: 'dist',
|
||||
@@ -54,12 +57,13 @@ export default [
|
||||
external,
|
||||
plugins: [
|
||||
json(),
|
||||
resolve(),
|
||||
resolve({
|
||||
extensions: ['.mjs', '.js', '.ts']
|
||||
}),
|
||||
commonjs(),
|
||||
typescript({
|
||||
typescript: require('typescript')
|
||||
sucrase({
|
||||
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 }) {
|
||||
const target = select_route(new URL(href, document.baseURI));
|
||||
const target = select_target(new URL(href, document.baseURI));
|
||||
|
||||
if (target) {
|
||||
history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
|
||||
12
runtime/src/app/index.ts
Normal file
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[]) {
|
||||
return pages
|
||||
.filter(route => {
|
||||
if (!pathnames) return true;
|
||||
return pathnames.some(pathname => route.pattern.test(pathname));
|
||||
})
|
||||
return routes
|
||||
.filter(pathnames
|
||||
? route => pathnames.some(pathname => route.pattern.test(pathname))
|
||||
: () => true
|
||||
)
|
||||
.reduce((promise: Promise<any>, route) => promise.then(() => {
|
||||
return Promise.all(route.parts.map(part => part && load_component(components[part.i])));
|
||||
}), Promise.resolve());
|
||||
@@ -5,26 +5,23 @@ import {
|
||||
navigate,
|
||||
scroll_history,
|
||||
scroll_state,
|
||||
select_route,
|
||||
set_store,
|
||||
select_target,
|
||||
handle_error,
|
||||
set_target,
|
||||
uid,
|
||||
set_uid,
|
||||
set_cid
|
||||
} from '../app';
|
||||
import prefetch from '../prefetch/index';
|
||||
import { Store } from '../types';
|
||||
|
||||
export default function start(opts: {
|
||||
target: Node,
|
||||
store?: (data: any) => Store
|
||||
target: Node
|
||||
}) {
|
||||
if ('scrollRestoration' in history) {
|
||||
history.scrollRestoration = 'manual';
|
||||
}
|
||||
|
||||
set_target(opts.target);
|
||||
if (opts.store) set_store(opts.store);
|
||||
|
||||
addEventListener('click', handle_click);
|
||||
addEventListener('popstate', handle_popstate);
|
||||
@@ -36,17 +33,14 @@ export default function start(opts: {
|
||||
return Promise.resolve().then(() => {
|
||||
const { hash, href } = location;
|
||||
|
||||
const deep_linked = hash && document.getElementById(hash.slice(1));
|
||||
scroll_history[uid] = deep_linked ?
|
||||
{ x: 0, y: deep_linked.getBoundingClientRect().top } :
|
||||
scroll_state();
|
||||
|
||||
history.replaceState({ id: uid }, '', href);
|
||||
|
||||
if (!initial_data.error) {
|
||||
const target = select_route(new URL(location.href));
|
||||
if (target) return navigate(target, uid);
|
||||
}
|
||||
const url = new URL(location.href);
|
||||
|
||||
if (initial_data.error) return handle_error(url);
|
||||
|
||||
const target = select_target(url);
|
||||
if (target) return navigate(target, uid, false, hash);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -84,7 +78,7 @@ function handle_click(event: MouseEvent) {
|
||||
const href = String(svg ? (<SVGAElement>a).href.baseVal : a.href);
|
||||
|
||||
if (href === location.href) {
|
||||
event.preventDefault();
|
||||
if (!location.hash) event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -101,9 +95,10 @@ function handle_click(event: MouseEvent) {
|
||||
// Don't handle hash changes
|
||||
if (url.pathname === location.pathname && url.search === location.search) return;
|
||||
|
||||
const target = select_route(url);
|
||||
const target = select_target(url);
|
||||
if (target) {
|
||||
navigate(target, null);
|
||||
const noscroll = a.hasAttribute('sapper-noscroll');
|
||||
navigate(target, null, noscroll, url.hash);
|
||||
event.preventDefault();
|
||||
history.pushState({ id: cid }, '', url.href);
|
||||
}
|
||||
@@ -123,7 +118,7 @@ function handle_popstate(event: PopStateEvent) {
|
||||
|
||||
if (event.state) {
|
||||
const url = new URL(location.href);
|
||||
const target = select_route(url);
|
||||
const target = select_target(url);
|
||||
if (target) {
|
||||
navigate(target, event.state.id);
|
||||
} else {
|
||||
@@ -135,4 +130,4 @@ function handle_popstate(event: PopStateEvent) {
|
||||
set_cid(uid);
|
||||
history.replaceState({ id: cid }, '', location.href);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,21 +8,14 @@ type Child = {
|
||||
component?: Component;
|
||||
};
|
||||
|
||||
export type RootProps = {
|
||||
path: string;
|
||||
params: Record<string, string>;
|
||||
query: Record<string, string>;
|
||||
child: Child;
|
||||
};
|
||||
|
||||
export interface ComponentConstructor {
|
||||
new (options: { target: Node, data: any, store: Store, hydrate: boolean }): Component;
|
||||
new (options: { target: Node, props: any, hydrate: boolean }): Component;
|
||||
preload: (props: { params: Params, query: Query }) => Promise<any>;
|
||||
};
|
||||
|
||||
export interface Component {
|
||||
set: (data: any) => void;
|
||||
destroy: () => void;
|
||||
$set: (data: any) => void;
|
||||
$destroy: () => void;
|
||||
}
|
||||
|
||||
export type ComponentLoader = {
|
||||
@@ -30,7 +23,7 @@ export type ComponentLoader = {
|
||||
css: string[]
|
||||
};
|
||||
|
||||
export type Page = {
|
||||
export type Route = {
|
||||
pattern: RegExp;
|
||||
parts: Array<{
|
||||
i: number;
|
||||
@@ -42,7 +35,7 @@ export type Manifest = {
|
||||
ignore: RegExp[];
|
||||
root: ComponentConstructor;
|
||||
error: () => Promise<{ default: ComponentConstructor }>;
|
||||
pages: Page[]
|
||||
pages: Route[]
|
||||
};
|
||||
|
||||
export type ScrollPosition = {
|
||||
@@ -51,11 +44,10 @@ export type ScrollPosition = {
|
||||
};
|
||||
|
||||
export type Target = {
|
||||
url: URL;
|
||||
path: string;
|
||||
page: Page;
|
||||
href: string;
|
||||
route: Route;
|
||||
match: RegExpExecArray;
|
||||
query: Record<string, string | true>;
|
||||
page: Page;
|
||||
};
|
||||
|
||||
export type Redirect = {
|
||||
@@ -63,6 +55,8 @@ export type Redirect = {
|
||||
location: string;
|
||||
};
|
||||
|
||||
export type Store = {
|
||||
get: () => any;
|
||||
}
|
||||
export type Page = {
|
||||
path: string;
|
||||
params: Record<string, string>;
|
||||
query: Record<string, string | string[]>;
|
||||
};
|
||||
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 * as path from 'path';
|
||||
import { writable } from 'svelte/store.mjs';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import cookie from 'cookie';
|
||||
import devalue from 'devalue';
|
||||
import fetch from 'node-fetch';
|
||||
import { URL } from 'url';
|
||||
import { build_dir, dev, src_dir, IGNORE } from '../placeholders';
|
||||
import { Manifest, Page, Props, Req, Res, Store } from './types';
|
||||
import URL from 'url';
|
||||
import { IGNORE } from '../constants';
|
||||
import { Manifest, Page, Props, Req, Res } from './types';
|
||||
import { build_dir, dev, src_dir } from '@sapper/internal/manifest-server';
|
||||
import { stores } from '@sapper/internal/shared';
|
||||
import App from '@sapper/internal/App.svelte';
|
||||
|
||||
export function get_page_handler(
|
||||
manifest: Manifest,
|
||||
store_getter: (req: Req, res: Res) => Store
|
||||
session_getter: (req: Req, res: Res) => any
|
||||
) {
|
||||
const get_build_info = dev
|
||||
? () => JSON.parse(fs.readFileSync(path.join(build_dir, 'build.json'), 'utf-8'))
|
||||
@@ -33,7 +37,8 @@ export function get_page_handler(
|
||||
}, req, res, statusCode, error || new Error('Unknown error in preload function'));
|
||||
}
|
||||
|
||||
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 is_service_worker_index = req.path === '/service-worker-index.html';
|
||||
const build_info: {
|
||||
bundler: 'rollup' | 'webpack',
|
||||
shimport: string | null,
|
||||
@@ -47,7 +52,7 @@ export function get_page_handler(
|
||||
// preload main.js and current route
|
||||
// TODO detect other stuff we can preload? images, CSS, fonts?
|
||||
let preloaded_chunks = Array.isArray(build_info.assets.main) ? build_info.assets.main : [build_info.assets.main];
|
||||
if (!error) {
|
||||
if (!error && !is_service_worker_index) {
|
||||
page.parts.forEach(part => {
|
||||
if (!part) return;
|
||||
|
||||
@@ -56,14 +61,27 @@ export function get_page_handler(
|
||||
});
|
||||
}
|
||||
|
||||
const link = preloaded_chunks
|
||||
.filter(file => file && !file.match(/\.map$/))
|
||||
.map(file => `<${req.baseUrl}/client/${file}>;rel="preload";as="script"`)
|
||||
.join(', ');
|
||||
if (build_info.bundler === 'rollup') {
|
||||
// TODO add dependencies and CSS
|
||||
const link = preloaded_chunks
|
||||
.filter(file => file && !file.match(/\.map$/))
|
||||
.map(file => `<${req.baseUrl}/client/${file}>;rel="modulepreload"`)
|
||||
.join(', ');
|
||||
|
||||
res.setHeader('Link', link);
|
||||
res.setHeader('Link', link);
|
||||
} else {
|
||||
const link = preloaded_chunks
|
||||
.filter(file => file && !file.match(/\.map$/))
|
||||
.map((file) => {
|
||||
const as = /\.css$/.test(file) ? 'style' : 'script';
|
||||
return `<${req.baseUrl}/client/${file}>;rel="preload";as="${as}"`;
|
||||
})
|
||||
.join(', ');
|
||||
|
||||
const store = store_getter ? store_getter(req, res) : null;
|
||||
res.setHeader('Link', link);
|
||||
}
|
||||
|
||||
const session = session_getter(req, res);
|
||||
|
||||
let redirect: { statusCode: number, location: string };
|
||||
let preload_error: { statusCode: number, message: Error | string };
|
||||
@@ -80,7 +98,7 @@ export function get_page_handler(
|
||||
preload_error = { statusCode, message };
|
||||
},
|
||||
fetch: (url: string, opts?: any) => {
|
||||
const parsed = new URL(url, `http://127.0.0.1:${process.env.PORT}${req.baseUrl ? req.baseUrl + '/' :''}`);
|
||||
const parsed = new URL.URL(url, `http://127.0.0.1:${process.env.PORT}${req.baseUrl ? req.baseUrl + '/' :''}`);
|
||||
|
||||
if (opts) {
|
||||
opts = Object.assign({}, opts);
|
||||
@@ -91,57 +109,75 @@ export function get_page_handler(
|
||||
);
|
||||
|
||||
if (include_cookies) {
|
||||
if (!opts.headers) opts.headers = {};
|
||||
opts.headers = Object.assign({}, opts.headers);
|
||||
|
||||
const str = []
|
||||
.concat(
|
||||
cookie.parse(req.headers.cookie || ''),
|
||||
cookie.parse(opts.headers.cookie || ''),
|
||||
cookie.parse(res.getHeader('Set-Cookie') || '')
|
||||
)
|
||||
.map(cookie => {
|
||||
return Object.keys(cookie)
|
||||
.map(name => `${name}=${encodeURIComponent(cookie[name])}`)
|
||||
.join('; ');
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
const cookies = Object.assign(
|
||||
{},
|
||||
cookie.parse(req.headers.cookie || ''),
|
||||
cookie.parse(opts.headers.cookie || '')
|
||||
);
|
||||
|
||||
const set_cookie = res.getHeader('Set-Cookie');
|
||||
(Array.isArray(set_cookie) ? set_cookie : [set_cookie]).forEach(str => {
|
||||
const match = /([^=]+)=([^;]+)/.exec(<string>str);
|
||||
if (match) cookies[match[1]] = match[2];
|
||||
});
|
||||
|
||||
const str = Object.keys(cookies)
|
||||
.map(key => `${key}=${cookies[key]}`)
|
||||
.join('; ');
|
||||
|
||||
opts.headers.cookie = str;
|
||||
}
|
||||
}
|
||||
|
||||
return fetch(parsed.href, opts);
|
||||
},
|
||||
store
|
||||
}
|
||||
};
|
||||
|
||||
const root_preloaded = manifest.root.preload
|
||||
? manifest.root.preload.call(preload_context, {
|
||||
path: req.path,
|
||||
query: req.query,
|
||||
params: {}
|
||||
})
|
||||
: {};
|
||||
let preloaded;
|
||||
let match;
|
||||
let params;
|
||||
|
||||
const match = error ? null : page.pattern.exec(req.path);
|
||||
|
||||
Promise.all([root_preloaded].concat(page.parts.map(part => {
|
||||
if (!part) return null;
|
||||
|
||||
return part.component.preload
|
||||
? part.component.preload.call(preload_context, {
|
||||
try {
|
||||
const root_preloaded = manifest.root_preload
|
||||
? manifest.root_preload.call(preload_context, {
|
||||
path: req.path,
|
||||
query: req.query,
|
||||
params: part.params ? part.params(match) : {}
|
||||
})
|
||||
params: {}
|
||||
}, session)
|
||||
: {};
|
||||
}))).catch(err => {
|
||||
|
||||
match = error ? null : page.pattern.exec(req.path);
|
||||
|
||||
|
||||
let toPreload = [root_preloaded];
|
||||
if (!is_service_worker_index) {
|
||||
toPreload = toPreload.concat(page.parts.map(part => {
|
||||
if (!part) return null;
|
||||
|
||||
// the deepest level is used below, to initialise the store
|
||||
params = part.params ? part.params(match) : {};
|
||||
|
||||
return part.preload
|
||||
? part.preload.call(preload_context, {
|
||||
path: req.path,
|
||||
query: req.query,
|
||||
params
|
||||
}, session)
|
||||
: {};
|
||||
}))
|
||||
}
|
||||
|
||||
preloaded = await Promise.all(toPreload);
|
||||
} catch (err) {
|
||||
preload_error = { statusCode: 500, message: err };
|
||||
return []; // appease TypeScript
|
||||
}).then(preloaded => {
|
||||
preloaded = []; // appease TypeScript
|
||||
}
|
||||
|
||||
try {
|
||||
if (redirect) {
|
||||
const location = `${req.baseUrl}/${redirect.location}`;
|
||||
const location = URL.resolve(req.baseUrl || '/', redirect.location);
|
||||
|
||||
res.statusCode = redirect.statusCode;
|
||||
res.setHeader('Location', location);
|
||||
@@ -155,61 +191,67 @@ export function get_page_handler(
|
||||
return;
|
||||
}
|
||||
|
||||
const serialized = {
|
||||
preloaded: `[${preloaded.map(data => try_serialize(data)).join(',')}]`,
|
||||
store: store && try_serialize(store.get())
|
||||
};
|
||||
|
||||
const segments = req.path.split('/').filter(Boolean);
|
||||
|
||||
const props: Props = {
|
||||
path: req.path,
|
||||
query: req.query,
|
||||
params: {},
|
||||
child: null
|
||||
// TODO make this less confusing
|
||||
const layout_segments = [segments[0]];
|
||||
let l = 1;
|
||||
|
||||
page.parts.forEach((part, i) => {
|
||||
layout_segments[l] = segments[i + 1];
|
||||
if (!part) return null;
|
||||
l++;
|
||||
});
|
||||
|
||||
const props = {
|
||||
segments: layout_segments,
|
||||
status: error ? status : 200,
|
||||
error: error ? error instanceof Error ? error : { message: error } : null,
|
||||
session: writable(session),
|
||||
level0: {
|
||||
props: preloaded[0]
|
||||
},
|
||||
level1: {
|
||||
segment: segments[0],
|
||||
props: {}
|
||||
}
|
||||
};
|
||||
|
||||
if (error) {
|
||||
props.error = error instanceof Error ? error : { message: error };
|
||||
props.status = status;
|
||||
}
|
||||
if (!is_service_worker_index) {
|
||||
let l = 1;
|
||||
for (let i = 0; i < page.parts.length; i += 1) {
|
||||
const part = page.parts[i];
|
||||
if (!part) continue;
|
||||
|
||||
const data = Object.assign({}, props, preloaded[0], {
|
||||
params: {},
|
||||
child: {
|
||||
segment: segments[0]
|
||||
props[`level${l++}`] = {
|
||||
component: part.component,
|
||||
props: preloaded[i + 1] || {},
|
||||
segment: segments[i]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
let level = data.child;
|
||||
for (let i = 0; i < page.parts.length; i += 1) {
|
||||
const part = page.parts[i];
|
||||
if (!part) continue;
|
||||
|
||||
const get_params = part.params || (() => ({}));
|
||||
|
||||
Object.assign(level, {
|
||||
component: part.component,
|
||||
props: Object.assign({}, props, {
|
||||
params: get_params(match)
|
||||
}, preloaded[i + 1])
|
||||
});
|
||||
|
||||
level.props.child = <Props["child"]>{
|
||||
segment: segments[i + 1]
|
||||
};
|
||||
level = level.props.child;
|
||||
}
|
||||
|
||||
const { html, head, css } = manifest.root.render(data, {
|
||||
store
|
||||
stores.page.set({
|
||||
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__={${[
|
||||
error && `error:1`,
|
||||
error && `error:${serialized.error},status:${status}`,
|
||||
`baseUrl:"${req.baseUrl}"`,
|
||||
serialized.preloaded && `preloaded:${serialized.preloaded}`,
|
||||
serialized.store && `store:${serialized.store}`
|
||||
serialized.session && `session:${serialized.session}`
|
||||
].filter(Boolean).join(',')}};`;
|
||||
|
||||
if (has_service_worker) {
|
||||
@@ -224,7 +266,7 @@ export function get_page_handler(
|
||||
const legacy_main = `${req.baseUrl}/client/legacy/${build_info.legacy_assets.main}`;
|
||||
script += `(function(){try{eval("async function x(){}");var main="${main}"}catch(e){main="${legacy_main}"};var s=document.createElement("script");try{new Function("if(0)import('')")();s.src=main;s.type="module";s.crossOrigin="use-credentials";}catch(e){s.src="${req.baseUrl}/client/shimport@${build_info.shimport}.js";s.setAttribute("data-main",main);}document.head.appendChild(s);}());`;
|
||||
} else {
|
||||
script += `var s=document.createElement("script");try{new Function("if(0)import('')")();s.src="${main}";s.type="module";s.crossOrigin="use-credentials";}catch(e){console.error(e);s.src="${req.baseUrl}/client/shimport@${build_info.shimport}.js";s.setAttribute("data-main","${main}")}document.head.appendChild(s)`;
|
||||
script += `var s=document.createElement("script");try{new Function("if(0)import('')")();s.src="${main}";s.type="module";s.crossOrigin="use-credentials";}catch(e){s.src="${req.baseUrl}/client/shimport@${build_info.shimport}.js";s.setAttribute("data-main","${main}")}document.head.appendChild(s)`;
|
||||
}
|
||||
} else {
|
||||
script += `</script><script src="${main}">`;
|
||||
@@ -267,7 +309,8 @@ export function get_page_handler(
|
||||
|
||||
res.statusCode = status;
|
||||
res.end(body);
|
||||
}).catch(err => {
|
||||
} catch(err) {
|
||||
console.log(err);
|
||||
if (error) {
|
||||
// we encountered an error while rendering the error page — oops
|
||||
res.statusCode = 500;
|
||||
@@ -275,18 +318,22 @@ export function get_page_handler(
|
||||
} else {
|
||||
handle_error(req, res, 500, err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return function find_route(req: Req, res: Res, next: () => void) {
|
||||
if (req[IGNORE]) return next();
|
||||
|
||||
if (!server_routes.some(route => route.pattern.test(req.path))) {
|
||||
for (const page of pages) {
|
||||
if (page.pattern.test(req.path)) {
|
||||
handle_page(page, req, res);
|
||||
return;
|
||||
}
|
||||
if (req.path === '/service-worker-index.html') {
|
||||
const homePage = pages.find(page => page.pattern.test('/'));
|
||||
handle_page(homePage, req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const page of pages) {
|
||||
if (page.pattern.test(req.path)) {
|
||||
handle_page(page, req, res);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,10 +345,11 @@ function read_template(dir = build_dir) {
|
||||
return fs.readFileSync(`${dir}/template.html`, 'utf-8');
|
||||
}
|
||||
|
||||
function try_serialize(data: any) {
|
||||
function try_serialize(data: any, fail?: (err) => void) {
|
||||
try {
|
||||
return devalue(data);
|
||||
} catch (err) {
|
||||
if (fail) fail(err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -316,4 +364,4 @@ function escape_html(html: string) {
|
||||
};
|
||||
|
||||
return html.replace(/["'&<>]/g, c => `&${chars[c]};`);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { IGNORE } from '../placeholders';
|
||||
import { IGNORE } from '../constants';
|
||||
import { Req, Res, ServerRoute } from './types';
|
||||
|
||||
export function get_server_route_handler(routes: ServerRoute[]) {
|
||||
function handle_route(route: ServerRoute, req: Req, res: Res, next: () => void) {
|
||||
async function handle_route(route: ServerRoute, req: Req, res: Res, next: () => void) {
|
||||
req.params = route.params(route.pattern.exec(req.path));
|
||||
|
||||
const method = req.method.toLowerCase();
|
||||
@@ -53,7 +53,7 @@ export function get_server_route_handler(routes: ServerRoute[]) {
|
||||
};
|
||||
|
||||
try {
|
||||
handle_method(req, res, handle_next);
|
||||
await handle_method(req, res, handle_next);
|
||||
} catch (err) {
|
||||
handle_next(err);
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { build_dir, dev, manifest, IGNORE } from '../placeholders';
|
||||
import { Handler, Req, Res, Store } from './types';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { build_dir, dev, manifest } from '@sapper/internal/manifest-server';
|
||||
import { Handler, Req, Res } from './types';
|
||||
import { get_server_route_handler } from './get_server_route_handler';
|
||||
import { get_page_handler } from './get_page_handler';
|
||||
import { lookup } from './mime';
|
||||
import { IGNORE } from '../constants';
|
||||
|
||||
export default function middleware(opts: {
|
||||
store?: (req: Req, res: Res) => Store,
|
||||
session?: (req: Req, res: Res) => any,
|
||||
ignore?: any
|
||||
} = {}) {
|
||||
const { store, ignore } = opts;
|
||||
const { session, ignore } = opts;
|
||||
|
||||
let emitted_basepath = false;
|
||||
|
||||
@@ -51,11 +52,6 @@ export default function middleware(opts: {
|
||||
next();
|
||||
},
|
||||
|
||||
fs.existsSync(path.join(build_dir, 'index.html')) && serve({
|
||||
pathname: '/index.html',
|
||||
cache_control: dev ? 'no-cache' : 'max-age=600'
|
||||
}),
|
||||
|
||||
fs.existsSync(path.join(build_dir, 'service-worker.js')) && serve({
|
||||
pathname: '/service-worker.js',
|
||||
cache_control: 'no-cache, no-store, must-revalidate'
|
||||
@@ -73,7 +69,7 @@ export default function middleware(opts: {
|
||||
|
||||
get_server_route_handler(manifest.server_routes),
|
||||
|
||||
get_page_handler(manifest, store)
|
||||
get_page_handler(manifest, session || noop)
|
||||
].filter(Boolean));
|
||||
}
|
||||
|
||||
@@ -140,4 +136,6 @@ export function serve({ prefix, pathname, cache_control }: {
|
||||
next();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function noop(){}
|
||||
@@ -12,6 +12,7 @@ export type Page = {
|
||||
name: string;
|
||||
component: Component;
|
||||
params?: (match: RegExpMatchArray) => Record<string, string>;
|
||||
preload?: (data: any) => any | Promise<any>;
|
||||
}>
|
||||
};
|
||||
|
||||
@@ -19,19 +20,13 @@ export type Manifest = {
|
||||
server_routes: ServerRoute[];
|
||||
pages: Page[];
|
||||
root: Component;
|
||||
root_preload?: (data: any) => any | Promise<any>;
|
||||
error: Component;
|
||||
}
|
||||
|
||||
export type Handler = (req: Req, res: Res, next: () => void) => void;
|
||||
|
||||
export type Store = {
|
||||
get: () => any
|
||||
};
|
||||
|
||||
export type Props = {
|
||||
path: string;
|
||||
query: Record<string, string>;
|
||||
params: Record<string, string>;
|
||||
error?: { message: string };
|
||||
status?: number;
|
||||
child: {
|
||||
@@ -60,10 +55,9 @@ export interface Res extends ServerResponse {
|
||||
export { ServerResponse };
|
||||
|
||||
interface Component {
|
||||
render: (data: any, opts: { store: Store }) => {
|
||||
render: (data: any) => {
|
||||
head: string;
|
||||
css: { code: string, map: any };
|
||||
html: string
|
||||
},
|
||||
preload: (data: any) => any | Promise<any>
|
||||
}
|
||||
}
|
||||
10
src/api.ts
10
src/api.ts
@@ -1,6 +1,4 @@
|
||||
import { dev } from './api/dev';
|
||||
import { build } from './api/build';
|
||||
import { exporter } from './api/export';
|
||||
import { find_page } from './api/find_page';
|
||||
|
||||
export { dev, build, exporter, find_page };
|
||||
export { dev } from './api/dev';
|
||||
export { build } from './api/build';
|
||||
export { export } from './api/export';
|
||||
export { find_page } from './api/find_page';
|
||||
122
src/api/build.ts
122
src/api/build.ts
@@ -1,45 +1,63 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import mkdirp from 'mkdirp';
|
||||
import rimraf from 'rimraf';
|
||||
import { EventEmitter } from 'events';
|
||||
import minify_html from './utils/minify_html';
|
||||
import { create_compilers, create_main_manifests, create_manifest_data, create_serviceworker_manifest } from '../core';
|
||||
import * as events from './interfaces';
|
||||
import { create_compilers, create_app, create_manifest_data, create_serviceworker_manifest } from '../core';
|
||||
import { copy_shimport } from './utils/copy_shimport';
|
||||
import { Dirs } from '../interfaces';
|
||||
import read_template from '../core/read_template';
|
||||
import { CompileResult } from '../core/create_compilers/interfaces';
|
||||
import { noop } from './utils/noop';
|
||||
import validate_bundler from './utils/validate_bundler';
|
||||
import { copy_runtime } from './utils/copy_runtime';
|
||||
import { rimraf, mkdirp } from './utils/fs_utils';
|
||||
|
||||
type Opts = {
|
||||
legacy: boolean;
|
||||
bundler: 'rollup' | 'webpack';
|
||||
cwd?: string;
|
||||
src?: string;
|
||||
routes?: string;
|
||||
dest?: string;
|
||||
output?: string;
|
||||
static?: string;
|
||||
legacy?: boolean;
|
||||
bundler?: 'rollup' | 'webpack';
|
||||
oncompile?: ({ type, result }: { type: string, result: CompileResult }) => void;
|
||||
};
|
||||
|
||||
export function build(opts: Opts, dirs: Dirs) {
|
||||
const emitter = new EventEmitter();
|
||||
export async function build({
|
||||
cwd,
|
||||
src = 'src',
|
||||
routes = 'src/routes',
|
||||
output = 'src/node_modules/@sapper',
|
||||
static: static_files = 'static',
|
||||
dest = '__sapper__/build',
|
||||
|
||||
execute(emitter, opts, dirs).then(
|
||||
() => {
|
||||
emitter.emit('done', <events.DoneEvent>{}); // TODO do we need to pass back any info?
|
||||
},
|
||||
error => {
|
||||
emitter.emit('error', <events.ErrorEvent>{
|
||||
error
|
||||
});
|
||||
}
|
||||
);
|
||||
bundler,
|
||||
legacy = false,
|
||||
oncompile = noop
|
||||
}: Opts = {}) {
|
||||
bundler = validate_bundler(bundler);
|
||||
|
||||
return emitter;
|
||||
}
|
||||
cwd = path.resolve(cwd);
|
||||
src = path.resolve(cwd, src);
|
||||
dest = path.resolve(cwd, dest);
|
||||
routes = path.resolve(cwd, routes);
|
||||
output = path.resolve(cwd, output);
|
||||
static_files = path.resolve(cwd, static_files);
|
||||
|
||||
async function execute(emitter: EventEmitter, opts: Opts, dirs: Dirs) {
|
||||
rimraf.sync(path.join(dirs.dest, '**/*'));
|
||||
mkdirp.sync(`${dirs.dest}/client`);
|
||||
copy_shimport(dirs.dest);
|
||||
if (legacy && bundler === 'webpack') {
|
||||
throw new Error(`Legacy builds are not supported for projects using webpack`);
|
||||
}
|
||||
|
||||
rimraf(output);
|
||||
mkdirp(output);
|
||||
copy_runtime(output);
|
||||
|
||||
rimraf(dest);
|
||||
mkdirp(`${dest}/client`);
|
||||
copy_shimport(dest);
|
||||
|
||||
// minify src/template.html
|
||||
// TODO compile this to a function? could be quicker than str.replace(...).replace(...).replace(...)
|
||||
const template = read_template();
|
||||
const template = read_template(src);
|
||||
|
||||
// remove this in a future version
|
||||
if (template.indexOf('%sapper.base%') === -1) {
|
||||
@@ -48,63 +66,75 @@ async function execute(emitter: EventEmitter, opts: Opts, dirs: Dirs) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
fs.writeFileSync(`${dirs.dest}/template.html`, minify_html(template));
|
||||
fs.writeFileSync(`${dest}/template.html`, minify_html(template));
|
||||
|
||||
const manifest_data = create_manifest_data();
|
||||
const manifest_data = create_manifest_data(routes);
|
||||
|
||||
// create src/manifest/client.js and src/manifest/server.js
|
||||
create_main_manifests({ bundler: opts.bundler, manifest_data });
|
||||
// create src/node_modules/@sapper/app.mjs and server.mjs
|
||||
create_app({
|
||||
bundler,
|
||||
manifest_data,
|
||||
cwd,
|
||||
src,
|
||||
dest,
|
||||
routes,
|
||||
output,
|
||||
dev: false
|
||||
});
|
||||
|
||||
const { client, server, serviceworker } = await create_compilers(opts.bundler);
|
||||
const { client, server, serviceworker } = await create_compilers(bundler, cwd, src, dest, true);
|
||||
|
||||
const client_result = await client.compile();
|
||||
emitter.emit('build', <events.BuildEvent>{
|
||||
oncompile({
|
||||
type: 'client',
|
||||
// TODO duration/warnings
|
||||
result: client_result
|
||||
});
|
||||
|
||||
const build_info = client_result.to_json(manifest_data, dirs);
|
||||
const build_info = client_result.to_json(manifest_data, { src, routes, dest });
|
||||
|
||||
if (opts.legacy) {
|
||||
if (legacy) {
|
||||
process.env.SAPPER_LEGACY_BUILD = 'true';
|
||||
const { client } = await create_compilers(opts.bundler);
|
||||
const { client } = await create_compilers(bundler, cwd, src, dest, true);
|
||||
|
||||
const client_result = await client.compile();
|
||||
|
||||
emitter.emit('build', <events.BuildEvent>{
|
||||
oncompile({
|
||||
type: 'client (legacy)',
|
||||
// TODO duration/warnings
|
||||
result: client_result
|
||||
});
|
||||
|
||||
client_result.to_json(manifest_data, dirs);
|
||||
client_result.to_json(manifest_data, { src, routes, dest });
|
||||
build_info.legacy_assets = client_result.assets;
|
||||
delete process.env.SAPPER_LEGACY_BUILD;
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.join(dirs.dest, 'build.json'), JSON.stringify(build_info));
|
||||
fs.writeFileSync(path.join(dest, 'build.json'), JSON.stringify(build_info));
|
||||
|
||||
const server_stats = await server.compile();
|
||||
emitter.emit('build', <events.BuildEvent>{
|
||||
oncompile({
|
||||
type: 'server',
|
||||
// TODO duration/warnings
|
||||
result: server_stats
|
||||
});
|
||||
|
||||
let serviceworker_stats;
|
||||
|
||||
if (serviceworker) {
|
||||
|
||||
const client_files = client_result.chunks
|
||||
.filter(chunk => !chunk.file.endsWith('.map')) // SW does not need to cache sourcemap files
|
||||
.map(chunk => `client/${chunk.file}`);
|
||||
|
||||
create_serviceworker_manifest({
|
||||
manifest_data,
|
||||
client_files: client_result.chunks.map(chunk => `client/${chunk.file}`)
|
||||
output,
|
||||
client_files,
|
||||
static_files
|
||||
});
|
||||
|
||||
serviceworker_stats = await serviceworker.compile();
|
||||
|
||||
emitter.emit('build', <events.BuildEvent>{
|
||||
oncompile({
|
||||
type: 'serviceworker',
|
||||
// TODO duration/warnings
|
||||
result: serviceworker_stats
|
||||
});
|
||||
}
|
||||
|
||||
165
src/api/dev.ts
165
src/api/dev.ts
@@ -3,32 +3,47 @@ import * as fs from 'fs';
|
||||
import * as http from 'http';
|
||||
import * as child_process from 'child_process';
|
||||
import * as ports from 'port-authority';
|
||||
import mkdirp from 'mkdirp';
|
||||
import rimraf from 'rimraf';
|
||||
import { locations } from '../config';
|
||||
import { EventEmitter } from 'events';
|
||||
import { create_manifest_data, create_main_manifests, create_compilers, create_serviceworker_manifest } from '../core';
|
||||
import { create_manifest_data, create_app, create_compilers, create_serviceworker_manifest } from '../core';
|
||||
import { Compiler, Compilers } from '../core/create_compilers';
|
||||
import { CompileResult, CompileError } from '../core/create_compilers/interfaces';
|
||||
import { CompileResult } from '../core/create_compilers/interfaces';
|
||||
import Deferred from './utils/Deferred';
|
||||
import * as events from './interfaces';
|
||||
import validate_bundler from '../cli/utils/validate_bundler';
|
||||
import validate_bundler from './utils/validate_bundler';
|
||||
import { copy_shimport } from './utils/copy_shimport';
|
||||
import { ManifestData } from '../interfaces';
|
||||
import { ManifestData, FatalEvent, ErrorEvent, ReadyEvent, InvalidEvent } from '../interfaces';
|
||||
import read_template from '../core/read_template';
|
||||
import { noop } from './utils/noop';
|
||||
import { copy_runtime } from './utils/copy_runtime';
|
||||
import { rimraf, mkdirp } from './utils/fs_utils';
|
||||
|
||||
export function dev(opts) {
|
||||
type Opts = {
|
||||
cwd?: string,
|
||||
src?: string,
|
||||
dest?: string,
|
||||
routes?: string,
|
||||
output?: string,
|
||||
static?: string,
|
||||
'dev-port'?: number,
|
||||
live?: boolean,
|
||||
hot?: boolean,
|
||||
'devtools-port'?: number,
|
||||
bundler?: 'rollup' | 'webpack',
|
||||
port?: number
|
||||
};
|
||||
|
||||
export function dev(opts: Opts) {
|
||||
return new Watcher(opts);
|
||||
}
|
||||
|
||||
class Watcher extends EventEmitter {
|
||||
bundler: string;
|
||||
bundler: 'rollup' | 'webpack';
|
||||
dirs: {
|
||||
cwd: string;
|
||||
src: string;
|
||||
dest: string;
|
||||
routes: string;
|
||||
rollup: string;
|
||||
webpack: string;
|
||||
output: string;
|
||||
static: string;
|
||||
}
|
||||
port: number;
|
||||
closed: boolean;
|
||||
@@ -54,34 +69,33 @@ class Watcher extends EventEmitter {
|
||||
}
|
||||
|
||||
constructor({
|
||||
src = locations.src(),
|
||||
dest = locations.dest(),
|
||||
routes = locations.routes(),
|
||||
cwd = '.',
|
||||
src = 'src',
|
||||
routes = 'src/routes',
|
||||
output = 'src/node_modules/@sapper',
|
||||
static: static_files = 'static',
|
||||
dest = '__sapper__/dev',
|
||||
'dev-port': dev_port,
|
||||
live,
|
||||
hot,
|
||||
'devtools-port': devtools_port,
|
||||
bundler,
|
||||
webpack = 'webpack',
|
||||
rollup = 'rollup',
|
||||
port = +process.env.PORT
|
||||
}: {
|
||||
src: string,
|
||||
dest: string,
|
||||
routes: string,
|
||||
'dev-port': number,
|
||||
live: boolean,
|
||||
hot: boolean,
|
||||
'devtools-port': number,
|
||||
bundler?: string,
|
||||
webpack: string,
|
||||
rollup: string,
|
||||
port: number
|
||||
}) {
|
||||
}: Opts) {
|
||||
super();
|
||||
|
||||
cwd = path.resolve(cwd);
|
||||
|
||||
this.bundler = validate_bundler(bundler);
|
||||
this.dirs = { src, dest, routes, webpack, rollup };
|
||||
this.dirs = {
|
||||
cwd,
|
||||
src: path.resolve(cwd, src),
|
||||
dest: path.resolve(cwd, dest),
|
||||
routes: path.resolve(cwd, routes),
|
||||
output: path.resolve(cwd, output),
|
||||
static: path.resolve(cwd, static_files)
|
||||
};
|
||||
|
||||
this.port = port;
|
||||
this.closed = false;
|
||||
|
||||
@@ -101,7 +115,7 @@ class Watcher extends EventEmitter {
|
||||
};
|
||||
|
||||
// remove this in a future version
|
||||
const template = read_template();
|
||||
const template = read_template(src);
|
||||
if (template.indexOf('%sapper.base%') === -1) {
|
||||
const error = new Error(`As of Sapper v0.10, your template.html file must include %sapper.base% in the <head>`);
|
||||
error.code = `missing-sapper-base`;
|
||||
@@ -120,7 +134,7 @@ class Watcher extends EventEmitter {
|
||||
async init() {
|
||||
if (this.port) {
|
||||
if (!await ports.check(this.port)) {
|
||||
this.emit('fatal', <events.FatalEvent>{
|
||||
this.emit('fatal', <FatalEvent>{
|
||||
message: `Port ${this.port} is unavailable`
|
||||
});
|
||||
return;
|
||||
@@ -129,9 +143,14 @@ class Watcher extends EventEmitter {
|
||||
this.port = await ports.find(3000);
|
||||
}
|
||||
|
||||
const { dest } = this.dirs;
|
||||
rimraf.sync(dest);
|
||||
mkdirp.sync(`${dest}/client`);
|
||||
const { cwd, src, dest, routes, output, static: static_files } = this.dirs;
|
||||
|
||||
rimraf(output);
|
||||
mkdirp(output);
|
||||
copy_runtime(output);
|
||||
|
||||
rimraf(dest);
|
||||
mkdirp(`${dest}/client`);
|
||||
if (this.bundler === 'rollup') copy_shimport(dest);
|
||||
|
||||
if (!this.dev_port) this.dev_port = await ports.find(10000);
|
||||
@@ -142,10 +161,16 @@ class Watcher extends EventEmitter {
|
||||
let manifest_data: ManifestData;
|
||||
|
||||
try {
|
||||
manifest_data = create_manifest_data();
|
||||
create_main_manifests({ bundler: this.bundler, manifest_data, dev_port: this.dev_port });
|
||||
manifest_data = create_manifest_data(routes);
|
||||
create_app({
|
||||
bundler: this.bundler,
|
||||
manifest_data,
|
||||
dev: true,
|
||||
dev_port: this.dev_port,
|
||||
cwd, src, dest, routes, output
|
||||
});
|
||||
} catch (err) {
|
||||
this.emit('fatal', <events.FatalEvent>{
|
||||
this.emit('fatal', <FatalEvent>{
|
||||
message: err.message
|
||||
});
|
||||
return;
|
||||
@@ -155,7 +180,7 @@ class Watcher extends EventEmitter {
|
||||
|
||||
this.filewatchers.push(
|
||||
watch_dir(
|
||||
locations.routes(),
|
||||
routes,
|
||||
({ path: file, stats }) => {
|
||||
if (stats.isDirectory()) {
|
||||
return path.basename(file)[0] !== '_';
|
||||
@@ -164,19 +189,26 @@ class Watcher extends EventEmitter {
|
||||
},
|
||||
() => {
|
||||
try {
|
||||
const new_manifest_data = create_manifest_data();
|
||||
create_main_manifests({ bundler: this.bundler, manifest_data, dev_port: this.dev_port });
|
||||
const new_manifest_data = create_manifest_data(routes);
|
||||
create_app({
|
||||
bundler: this.bundler,
|
||||
manifest_data, // TODO is this right? not new_manifest_data?
|
||||
dev: true,
|
||||
dev_port: this.dev_port,
|
||||
cwd, src, dest, routes, output
|
||||
});
|
||||
|
||||
manifest_data = new_manifest_data;
|
||||
} catch (err) {
|
||||
this.emit('error', <events.ErrorEvent>{
|
||||
message: err.message
|
||||
} catch (error) {
|
||||
this.emit('error', <ErrorEvent>{
|
||||
type: 'manifest',
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
),
|
||||
|
||||
fs.watch(`${locations.src()}/template.html`, () => {
|
||||
fs.watch(`${src}/template.html`, () => {
|
||||
this.dev_server.send({
|
||||
action: 'reload'
|
||||
});
|
||||
@@ -186,14 +218,11 @@ class Watcher extends EventEmitter {
|
||||
let deferred = new Deferred();
|
||||
|
||||
// TODO watch the configs themselves?
|
||||
const compilers: Compilers = await create_compilers(this.bundler, this.dirs);
|
||||
|
||||
let log = '';
|
||||
const compilers: Compilers = await create_compilers(this.bundler, cwd, src, dest, false);
|
||||
|
||||
const emitFatal = () => {
|
||||
this.emit('fatal', <events.FatalEvent>{
|
||||
message: `Server crashed`,
|
||||
log
|
||||
this.emit('fatal', <FatalEvent>{
|
||||
message: `Server crashed`
|
||||
});
|
||||
|
||||
this.crashed = true;
|
||||
@@ -210,12 +239,11 @@ class Watcher extends EventEmitter {
|
||||
handle_result: (result: CompileResult) => {
|
||||
deferred.promise.then(() => {
|
||||
const restart = () => {
|
||||
log = '';
|
||||
this.crashed = false;
|
||||
|
||||
ports.wait(this.port)
|
||||
.then((() => {
|
||||
this.emit('ready', <events.ReadyEvent>{
|
||||
this.emit('ready', <ReadyEvent>{
|
||||
port: this.port,
|
||||
process: this.proc
|
||||
});
|
||||
@@ -233,9 +261,8 @@ class Watcher extends EventEmitter {
|
||||
.catch(err => {
|
||||
if (this.crashed) return;
|
||||
|
||||
this.emit('fatal', <events.FatalEvent>{
|
||||
message: `Server is not listening on port ${this.port}`,
|
||||
log
|
||||
this.emit('fatal', <FatalEvent>{
|
||||
message: `Server is not listening on port ${this.port}`
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -266,12 +293,10 @@ class Watcher extends EventEmitter {
|
||||
});
|
||||
|
||||
this.proc.stdout.on('data', chunk => {
|
||||
log += chunk;
|
||||
this.emit('stdout', chunk);
|
||||
});
|
||||
|
||||
this.proc.stderr.on('data', chunk => {
|
||||
log += chunk;
|
||||
this.emit('stderr', chunk);
|
||||
});
|
||||
|
||||
@@ -312,7 +337,9 @@ class Watcher extends EventEmitter {
|
||||
|
||||
create_serviceworker_manifest({
|
||||
manifest_data,
|
||||
client_files
|
||||
output,
|
||||
client_files,
|
||||
static_files
|
||||
});
|
||||
|
||||
deferred.fulfil();
|
||||
@@ -361,7 +388,7 @@ class Watcher extends EventEmitter {
|
||||
};
|
||||
|
||||
process.nextTick(() => {
|
||||
this.emit('invalid', <events.InvalidEvent>{
|
||||
this.emit('invalid', <InvalidEvent>{
|
||||
changed: Array.from(this.current_build.changed),
|
||||
invalid: {
|
||||
server: this.current_build.rebuilding.has('server'),
|
||||
@@ -382,11 +409,11 @@ class Watcher extends EventEmitter {
|
||||
}) {
|
||||
compiler.oninvalid(invalid);
|
||||
|
||||
compiler.watch((err?: Error, result?: CompileResult) => {
|
||||
if (err) {
|
||||
this.emit('error', <events.ErrorEvent>{
|
||||
compiler.watch((error?: Error, result?: CompileResult) => {
|
||||
if (error) {
|
||||
this.emit('error', <ErrorEvent>{
|
||||
type: name,
|
||||
message: err.message
|
||||
error
|
||||
});
|
||||
} else {
|
||||
this.emit('build', {
|
||||
@@ -455,22 +482,20 @@ class DevServer {
|
||||
}
|
||||
}
|
||||
|
||||
function noop() {}
|
||||
|
||||
function watch_dir(
|
||||
dir: string,
|
||||
filter: ({ path, stats }: { path: string, stats: fs.Stats }) => boolean,
|
||||
callback: () => void
|
||||
) {
|
||||
let watch;
|
||||
let watch: any;
|
||||
let closed = false;
|
||||
|
||||
import('cheap-watch').then(CheapWatch => {
|
||||
import('cheap-watch').then(({ default: CheapWatch }) => {
|
||||
if (closed) return;
|
||||
|
||||
watch = new CheapWatch({ dir, filter, debounce: 50 });
|
||||
|
||||
watch.on('+', ({ isNew }) => {
|
||||
watch.on('+', ({ isNew }: { isNew: boolean }) => {
|
||||
if (isNew) callback();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,39 +1,33 @@
|
||||
import * as child_process from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as sander from 'sander';
|
||||
import * as url from 'url';
|
||||
import fetch from 'node-fetch';
|
||||
import * as yootils from 'yootils';
|
||||
import * as ports from 'port-authority';
|
||||
import { EventEmitter } from 'events';
|
||||
import clean_html from './utils/clean_html';
|
||||
import minify_html from './utils/minify_html';
|
||||
import Deferred from './utils/Deferred';
|
||||
import * as events from './interfaces';
|
||||
import { noop } from './utils/noop';
|
||||
import { parse as parseLinkHeader } from 'http-link-header';
|
||||
import { rimraf, copy, mkdirp } from './utils/fs_utils';
|
||||
|
||||
type Opts = {
|
||||
build: string,
|
||||
dest: string,
|
||||
static: string,
|
||||
build_dir?: string,
|
||||
export_dir?: string,
|
||||
cwd?: string,
|
||||
static?: string,
|
||||
basepath?: string,
|
||||
timeout: number | false
|
||||
timeout?: number | false,
|
||||
oninfo?: ({ message }: { message: string }) => void;
|
||||
onfile?: ({ file, size, status }: { file: string, size: number, status: number }) => void;
|
||||
};
|
||||
|
||||
export function exporter(opts: Opts) {
|
||||
const emitter = new EventEmitter();
|
||||
|
||||
execute(emitter, opts).then(
|
||||
() => {
|
||||
emitter.emit('done', <events.DoneEvent>{}); // TODO do we need to pass back any info?
|
||||
},
|
||||
error => {
|
||||
emitter.emit('error', <events.ErrorEvent>{
|
||||
error
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return emitter;
|
||||
}
|
||||
type Ref = {
|
||||
uri: string,
|
||||
rel: string,
|
||||
as: string
|
||||
};
|
||||
|
||||
function resolve(from: string, to: string) {
|
||||
return url.parse(url.resolve(from, to));
|
||||
@@ -41,42 +35,52 @@ function resolve(from: string, to: string) {
|
||||
|
||||
type URL = url.UrlWithStringQuery;
|
||||
|
||||
async function execute(emitter: EventEmitter, opts: Opts) {
|
||||
const export_dir = path.join(opts.dest, opts.basepath);
|
||||
export { _export as export };
|
||||
|
||||
async function _export({
|
||||
cwd,
|
||||
static: static_files = 'static',
|
||||
build_dir = '__sapper__/build',
|
||||
export_dir = '__sapper__/export',
|
||||
basepath = '',
|
||||
timeout = 5000,
|
||||
oninfo = noop,
|
||||
onfile = noop
|
||||
}: Opts = {}) {
|
||||
basepath = basepath.replace(/^\//, '')
|
||||
|
||||
cwd = path.resolve(cwd);
|
||||
static_files = path.resolve(cwd, static_files);
|
||||
build_dir = path.resolve(cwd, build_dir);
|
||||
export_dir = path.resolve(cwd, export_dir, basepath);
|
||||
|
||||
// Prep output directory
|
||||
sander.rimrafSync(export_dir);
|
||||
rimraf(export_dir);
|
||||
|
||||
sander.copydirSync(opts.static).to(export_dir);
|
||||
sander.copydirSync(opts.build, 'client').to(export_dir, 'client');
|
||||
copy(static_files, export_dir);
|
||||
copy(path.join(build_dir, 'client'), path.join(export_dir, 'client'));
|
||||
copy(path.join(build_dir, 'service-worker.js'), path.join(export_dir, 'service-worker.js'));
|
||||
copy(path.join(build_dir, 'service-worker.js.map'), path.join(export_dir, 'service-worker.js.map'));
|
||||
|
||||
if (sander.existsSync(opts.build, 'service-worker.js')) {
|
||||
sander.copyFileSync(opts.build, 'service-worker.js').to(export_dir, 'service-worker.js');
|
||||
}
|
||||
|
||||
if (sander.existsSync(opts.build, 'service-worker.js.map')) {
|
||||
sander.copyFileSync(opts.build, 'service-worker.js.map').to(export_dir, 'service-worker.js.map');
|
||||
}
|
||||
|
||||
const port = await ports.find(3000);
|
||||
const defaultPort = process.env.PORT ? parseInt(process.env.PORT) : 3000;
|
||||
const port = await ports.find(defaultPort);
|
||||
|
||||
const protocol = 'http:';
|
||||
const host = `localhost:${port}`;
|
||||
const origin = `${protocol}//${host}`;
|
||||
|
||||
const root = resolve(origin, opts.basepath || '');
|
||||
const root = resolve(origin, basepath);
|
||||
if (!root.href.endsWith('/')) root.href += '/';
|
||||
|
||||
emitter.emit('info', {
|
||||
oninfo({
|
||||
message: `Crawling ${root.href}`
|
||||
});
|
||||
|
||||
const proc = child_process.fork(path.resolve(`${opts.build}/server/server.js`), [], {
|
||||
cwd: process.cwd(),
|
||||
const proc = child_process.fork(path.resolve(`${build_dir}/server/server.js`), [], {
|
||||
cwd,
|
||||
env: Object.assign({
|
||||
PORT: port,
|
||||
NODE_ENV: 'production',
|
||||
SAPPER_DEST: opts.build,
|
||||
SAPPER_EXPORT: 'true'
|
||||
}, process.env)
|
||||
});
|
||||
@@ -84,8 +88,8 @@ async function execute(emitter: EventEmitter, opts: Opts) {
|
||||
const seen = new Set();
|
||||
const saved = new Set();
|
||||
|
||||
function save(path: string, status: number, type: string, body: string) {
|
||||
const { pathname } = resolve(origin, path);
|
||||
function save(url: string, status: number, type: string, body: string) {
|
||||
const { pathname } = resolve(origin, url);
|
||||
let file = decodeURIComponent(pathname.slice(1));
|
||||
|
||||
if (saved.has(file)) return;
|
||||
@@ -94,17 +98,21 @@ async function execute(emitter: EventEmitter, opts: Opts) {
|
||||
const is_html = type === 'text/html';
|
||||
|
||||
if (is_html) {
|
||||
file = file === '' ? 'index.html' : `${file}/index.html`;
|
||||
if (pathname !== '/service-worker-index.html') {
|
||||
file = file === '' ? 'index.html' : `${file}/index.html`;
|
||||
}
|
||||
body = minify_html(body);
|
||||
}
|
||||
|
||||
emitter.emit('file', <events.FileEvent>{
|
||||
onfile({
|
||||
file,
|
||||
size: body.length,
|
||||
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 => {
|
||||
@@ -113,15 +121,18 @@ async function execute(emitter: EventEmitter, opts: Opts) {
|
||||
});
|
||||
|
||||
async function handle(url: URL) {
|
||||
const pathname = (url.pathname.replace(root.pathname, '') || '/');
|
||||
let pathname = url.pathname;
|
||||
if (pathname !== '/service-worker-index.html') {
|
||||
pathname = pathname.replace(root.pathname, '') || '/'
|
||||
}
|
||||
|
||||
if (seen.has(pathname)) return;
|
||||
seen.add(pathname);
|
||||
|
||||
const timeout_deferred = new Deferred();
|
||||
const timeout = setTimeout(() => {
|
||||
const the_timeout = setTimeout(() => {
|
||||
timeout_deferred.reject(new Error(`Timed out waiting for ${url.href}`));
|
||||
}, opts.timeout);
|
||||
}, timeout);
|
||||
|
||||
const r = await Promise.race([
|
||||
fetch(url.href, {
|
||||
@@ -130,40 +141,51 @@ async function execute(emitter: EventEmitter, opts: Opts) {
|
||||
timeout_deferred.promise
|
||||
]);
|
||||
|
||||
clearTimeout(timeout); // prevent it hanging at the end
|
||||
clearTimeout(the_timeout); // prevent it hanging at the end
|
||||
|
||||
let type = r.headers.get('Content-Type');
|
||||
|
||||
let body = await r.text();
|
||||
|
||||
const range = ~~(r.status / 100);
|
||||
|
||||
if (range === 2) {
|
||||
if (type === 'text/html') {
|
||||
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_href = base_match && get_href(base_match[1]);
|
||||
const base = resolve(url.href, base_href);
|
||||
const base_match = /<base ([\s\S]+?)>/m.exec(cleaned);
|
||||
const base_href = base_match && get_href(base_match[1]);
|
||||
const base = resolve(url.href, base_href);
|
||||
|
||||
let match;
|
||||
let pattern = /<a ([\s\S]+?)>/gm;
|
||||
let match;
|
||||
let pattern = /<a ([\s\S]+?)>/gm;
|
||||
|
||||
while (match = pattern.exec(cleaned)) {
|
||||
const attrs = match[1];
|
||||
const href = get_href(attrs);
|
||||
while (match = pattern.exec(cleaned)) {
|
||||
const attrs = match[1];
|
||||
const href = get_href(attrs);
|
||||
|
||||
if (href) {
|
||||
const url = resolve(base.href, href);
|
||||
if (href) {
|
||||
const url = resolve(base.href, href);
|
||||
|
||||
if (url.protocol === protocol && url.host === host) {
|
||||
urls.push(url);
|
||||
if (url.protocol === protocol && url.host === host) {
|
||||
q.add(() => handle(url));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(urls.map(handle));
|
||||
await q.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,6 +203,7 @@ async function execute(emitter: EventEmitter, opts: Opts) {
|
||||
|
||||
return ports.wait(port)
|
||||
.then(() => handle(root))
|
||||
.then(() => handle(resolve(root.href, 'service-worker-index.html')))
|
||||
.then(() => proc.kill())
|
||||
.catch(err => {
|
||||
proc.kill();
|
||||
@@ -189,6 +212,6 @@ async function execute(emitter: EventEmitter, opts: Opts) {
|
||||
}
|
||||
|
||||
function get_href(attrs: string) {
|
||||
const match = /href\s*=\s*(?:"(.+?)"|'(.+?)'|([^\s>]+))/.exec(attrs);
|
||||
return match[1] || match[2] || match[3];
|
||||
}
|
||||
const match = /href\s*=\s*(?:"(.*?)"|'(.*?)'|([^\s>]*))/.exec(attrs);
|
||||
return match && (match[1] || match[2] || match[3]);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { locations } from '../config';
|
||||
import { create_manifest_data } from '../core';
|
||||
|
||||
export function find_page(pathname: string, cwd = locations.routes()) {
|
||||
export function find_page(pathname: string, cwd = 'src/routes') {
|
||||
const { pages } = create_manifest_data(cwd);
|
||||
|
||||
for (let i = 0; i < pages.length; i += 1) {
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import * as child_process from 'child_process';
|
||||
import { CompileResult } from '../core/create_compilers/interfaces';
|
||||
|
||||
export type ReadyEvent = {
|
||||
port: number;
|
||||
process: child_process.ChildProcess;
|
||||
};
|
||||
|
||||
export type ErrorEvent = {
|
||||
type: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type FatalEvent = {
|
||||
message: string;
|
||||
log?: string;
|
||||
};
|
||||
|
||||
export type InvalidEvent = {
|
||||
changed: string[];
|
||||
invalid: {
|
||||
client: boolean;
|
||||
server: boolean;
|
||||
serviceworker: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export type BuildEvent = {
|
||||
type: string;
|
||||
errors: Array<{ file: string, message: string, duplicate: boolean }>;
|
||||
warnings: Array<{ file: string, message: string, duplicate: boolean }>;
|
||||
duration: number;
|
||||
result: CompileResult;
|
||||
}
|
||||
|
||||
export type FileEvent = {
|
||||
file: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export type FailureEvent = {
|
||||
|
||||
}
|
||||
|
||||
export type DoneEvent = {};
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ export default function minify_html(html: string) {
|
||||
conservativeCollapse: true,
|
||||
decodeEntities: true,
|
||||
html5: true,
|
||||
ignoreCustomComments: [/^#/],
|
||||
minifyCSS: true,
|
||||
minifyJS: false,
|
||||
removeAttributeQuotes: true,
|
||||
|
||||
1
src/api/utils/noop.ts
Normal file
1
src/api/utils/noop.ts
Normal file
@@ -0,0 +1 @@
|
||||
export function noop() {}
|
||||
257
src/cli.ts
257
src/cli.ts
@@ -2,11 +2,21 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import sade from 'sade';
|
||||
import colors from 'kleur';
|
||||
import prettyMs from 'pretty-ms';
|
||||
import * as pkg from '../package.json';
|
||||
import { elapsed, repeat, left_pad, format_milliseconds } from './utils';
|
||||
import { InvalidEvent, ErrorEvent, FatalEvent, BuildEvent, ReadyEvent } from './interfaces';
|
||||
|
||||
const prog = sade('sapper').version(pkg.version);
|
||||
|
||||
if (process.argv[2] === 'start') {
|
||||
// remove this in a future version
|
||||
console.error(colors.bold().red(`'sapper start' has been removed`));
|
||||
console.error(`Use 'node [build_dir]' instead`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
prog.command('dev')
|
||||
.describe('Start a development server')
|
||||
.option('-p, --port', 'Specify a port')
|
||||
@@ -15,16 +25,121 @@ prog.command('dev')
|
||||
.option('--hot', 'Use hot module replacement (requires webpack)', true)
|
||||
.option('--live', 'Reload on changes if not using --hot', true)
|
||||
.option('--bundler', 'Specify a bundler (rollup or webpack)')
|
||||
.option('--cwd', 'Current working directory', '.')
|
||||
.option('--src', 'Source directory', 'src')
|
||||
.option('--routes', 'Routes directory', 'src/routes')
|
||||
.option('--static', 'Static files directory', 'static')
|
||||
.option('--output', 'Sapper output directory', 'src/node_modules/@sapper')
|
||||
.option('--build-dir', 'Development build directory', '__sapper__/dev')
|
||||
.action(async (opts: {
|
||||
port: number,
|
||||
open: boolean,
|
||||
'dev-port': number,
|
||||
live: boolean,
|
||||
hot: boolean,
|
||||
bundler?: string
|
||||
bundler?: 'rollup' | 'webpack',
|
||||
cwd: string,
|
||||
src: string,
|
||||
routes: string,
|
||||
static: string,
|
||||
output: string,
|
||||
'build-dir': string
|
||||
}) => {
|
||||
const { dev } = await import('./cli/dev');
|
||||
dev(opts);
|
||||
const { dev } = await import('./api/dev');
|
||||
|
||||
try {
|
||||
const watcher = dev({
|
||||
cwd: opts.cwd,
|
||||
src: opts.src,
|
||||
routes: opts.routes,
|
||||
static: opts.static,
|
||||
output: opts.output,
|
||||
dest: opts['build-dir'],
|
||||
port: opts.port,
|
||||
'dev-port': opts['dev-port'],
|
||||
live: opts.live,
|
||||
hot: opts.hot,
|
||||
bundler: opts.bundler
|
||||
});
|
||||
|
||||
let first = true;
|
||||
|
||||
watcher.on('stdout', data => {
|
||||
process.stdout.write(data);
|
||||
});
|
||||
|
||||
watcher.on('stderr', data => {
|
||||
process.stderr.write(data);
|
||||
});
|
||||
|
||||
watcher.on('ready', async (event: ReadyEvent) => {
|
||||
if (first) {
|
||||
console.log(colors.bold().cyan(`> Listening on http://localhost:${event.port}`));
|
||||
if (opts.open) {
|
||||
const { exec } = await import('child_process');
|
||||
exec(`open http://localhost:${event.port}`);
|
||||
}
|
||||
first = false;
|
||||
}
|
||||
});
|
||||
|
||||
watcher.on('invalid', (event: InvalidEvent) => {
|
||||
const changed = event.changed.map(filename => path.relative(process.cwd(), filename)).join(', ');
|
||||
console.log(`\n${colors.bold().cyan(changed)} changed. rebuilding...`);
|
||||
});
|
||||
|
||||
watcher.on('error', (event: ErrorEvent) => {
|
||||
const { type, error } = event;
|
||||
|
||||
console.log(colors.bold().red(`✗ ${type}`));
|
||||
|
||||
if (error.loc && error.loc.file) {
|
||||
console.log(colors.bold(`${path.relative(process.cwd(), error.loc.file)} (${error.loc.line}:${error.loc.column})`));
|
||||
}
|
||||
|
||||
console.log(colors.red(event.error.message));
|
||||
if (error.frame) console.log(error.frame);
|
||||
});
|
||||
|
||||
watcher.on('fatal', (event: FatalEvent) => {
|
||||
console.log(colors.bold().red(`> ${event.message}`));
|
||||
if (event.log) console.log(event.log);
|
||||
});
|
||||
|
||||
watcher.on('build', (event: BuildEvent) => {
|
||||
if (event.errors.length) {
|
||||
console.log(colors.bold().red(`✗ ${event.type}`));
|
||||
|
||||
event.errors.filter(e => !e.duplicate).forEach(error => {
|
||||
if (error.file) console.log(colors.bold(error.file));
|
||||
console.log(error.message);
|
||||
});
|
||||
|
||||
const hidden = event.errors.filter(e => e.duplicate).length;
|
||||
if (hidden > 0) {
|
||||
console.log(`${hidden} duplicate ${hidden === 1 ? 'error' : 'errors'} hidden\n`);
|
||||
}
|
||||
} else if (event.warnings.length) {
|
||||
console.log(colors.bold().yellow(`• ${event.type}`));
|
||||
|
||||
event.warnings.filter(e => !e.duplicate).forEach(warning => {
|
||||
if (warning.file) console.log(colors.bold(warning.file));
|
||||
console.log(warning.message);
|
||||
});
|
||||
|
||||
const hidden = event.warnings.filter(e => e.duplicate).length;
|
||||
if (hidden > 0) {
|
||||
console.log(`${hidden} duplicate ${hidden === 1 ? 'warning' : 'warnings'} hidden\n`);
|
||||
}
|
||||
} else {
|
||||
console.log(`${colors.bold().green(`✔ ${event.type}`)} ${colors.gray(`(${format_milliseconds(event.duration)})`)}`);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(colors.bold().red(`> ${err.message}`));
|
||||
console.log(colors.gray(err.stack));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
prog.command('build [dest]')
|
||||
@@ -32,93 +147,149 @@ prog.command('build [dest]')
|
||||
.option('-p, --port', 'Default of process.env.PORT', '3000')
|
||||
.option('--bundler', 'Specify a bundler (rollup or webpack, blank for auto)')
|
||||
.option('--legacy', 'Create separate legacy build')
|
||||
.option('--cwd', 'Current working directory', '.')
|
||||
.option('--src', 'Source directory', 'src')
|
||||
.option('--routes', 'Routes directory', 'src/routes')
|
||||
.option('--output', 'Sapper output directory', 'src/node_modules/@sapper')
|
||||
.example(`build custom-dir -p 4567`)
|
||||
.action(async (dest = '__sapper__/build', opts: {
|
||||
port: string,
|
||||
legacy: boolean,
|
||||
bundler?: string
|
||||
bundler?: 'rollup' | 'webpack',
|
||||
cwd: string,
|
||||
src: string,
|
||||
routes: string,
|
||||
output: string
|
||||
}) => {
|
||||
console.log(`> Building...`);
|
||||
|
||||
process.env.NODE_ENV = process.env.NODE_ENV || 'production';
|
||||
process.env.SAPPER_DEST = dest;
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
try {
|
||||
const { build } = await import('./cli/build');
|
||||
await build(opts);
|
||||
await _build(opts.bundler, opts.legacy, opts.cwd, opts.src, opts.routes, opts.output, dest);
|
||||
|
||||
const launcher = path.resolve(dest, 'index.js');
|
||||
|
||||
fs.writeFileSync(launcher, `
|
||||
// generated by sapper build at ${new Date().toISOString()}
|
||||
process.env.NODE_ENV = process.env.NODE_ENV || 'production';
|
||||
process.env.SAPPER_DEST = __dirname;
|
||||
process.env.PORT = process.env.PORT || ${opts.port || 3000};
|
||||
|
||||
console.log('Starting server on port ' + process.env.PORT);
|
||||
require('./server/server.js');
|
||||
`.replace(/^\t+/gm, '').trim());
|
||||
|
||||
console.error(`\n> Finished in ${elapsed(start)}. Type ${colors.bold.cyan(`node ${dest}`)} to run the app.`);
|
||||
console.error(`\n> Finished in ${elapsed(start)}. Type ${colors.bold().cyan(`node ${dest}`)} to run the app.`);
|
||||
} catch (err) {
|
||||
console.log(`${colors.bold.red(`> ${err.message}`)}`);
|
||||
console.log(`${colors.bold().red(`> ${err.message}`)}`);
|
||||
console.log(colors.gray(err.stack));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
prog.command('start [dir]')
|
||||
.describe('Start your app')
|
||||
.option('-p, --port', 'Specify a port')
|
||||
.option('-o, --open', 'Open a browser window')
|
||||
.action(async (dir = 'build', opts: { port: number, open: boolean }) => {
|
||||
const { start } = await import('./cli/start');
|
||||
start(dir, opts);
|
||||
});
|
||||
|
||||
prog.command('export [dest]')
|
||||
.describe('Export your app as static files (if possible)')
|
||||
.option('--build', '(Re)build app before exporting', true)
|
||||
.option('--build-dir', 'Specify a custom temporary build directory', '__sapper__/build')
|
||||
.option('--basepath', 'Specify a base path')
|
||||
.option('--timeout', 'Milliseconds to wait for a page (--no-timeout to disable)', 5000)
|
||||
.option('--legacy', 'Create separate legacy build')
|
||||
.option('--bundler', 'Specify a bundler (rollup or webpack, blank for auto)')
|
||||
.option('--cwd', 'Current working directory', '.')
|
||||
.option('--src', 'Source directory', 'src')
|
||||
.option('--routes', 'Routes directory', 'src/routes')
|
||||
.option('--static', 'Static files directory', 'static')
|
||||
.option('--output', 'Sapper output directory', 'src/node_modules/@sapper')
|
||||
.option('--build-dir', 'Intermediate build directory', '__sapper__/build')
|
||||
.action(async (dest = '__sapper__/export', opts: {
|
||||
build: boolean,
|
||||
legacy: boolean,
|
||||
bundler?: string,
|
||||
'build-dir': string,
|
||||
bundler?: 'rollup' | 'webpack',
|
||||
basepath?: string,
|
||||
timeout: number | false
|
||||
timeout: number | false,
|
||||
cwd: string,
|
||||
src: string,
|
||||
routes: string,
|
||||
static: string,
|
||||
output: string,
|
||||
'build-dir': string,
|
||||
}) => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
process.env.SAPPER_DEST = opts['build-dir'];
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
try {
|
||||
if (opts.build) {
|
||||
console.log(`> Building...`);
|
||||
const { build } = await import('./cli/build');
|
||||
await build(opts);
|
||||
await _build(opts.bundler, opts.legacy, opts.cwd, opts.src, opts.routes, opts.output, opts['build-dir']);
|
||||
console.error(`\n> Built in ${elapsed(start)}`);
|
||||
}
|
||||
|
||||
const { exporter } = await import('./cli/export');
|
||||
await exporter(dest, opts);
|
||||
console.error(`\n> Finished in ${elapsed(start)}. Type ${colors.bold.cyan(`npx serve ${dest}`)} to run the app.`);
|
||||
const { export: _export } = await import('./api/export');
|
||||
const { default: pb } = await import('pretty-bytes');
|
||||
|
||||
await _export({
|
||||
cwd: opts.cwd,
|
||||
static: opts.static,
|
||||
build_dir: opts['build-dir'],
|
||||
export_dir: dest,
|
||||
basepath: opts.basepath,
|
||||
timeout: opts.timeout,
|
||||
|
||||
oninfo: event => {
|
||||
console.log(colors.bold().cyan(`> ${event.message}`));
|
||||
},
|
||||
|
||||
onfile: event => {
|
||||
const size_color = event.size > 150000 ? colors.bold().red : event.size > 50000 ? colors.bold().yellow : colors.bold().gray;
|
||||
const size_label = size_color(left_pad(pb(event.size), 10));
|
||||
|
||||
const file_label = event.status === 200
|
||||
? event.file
|
||||
: colors.bold()[event.status >= 400 ? 'red' : 'yellow'](`(${event.status}) ${event.file}`);
|
||||
|
||||
console.log(`${size_label} ${file_label}`);
|
||||
}
|
||||
});
|
||||
|
||||
console.error(`\n> Finished in ${elapsed(start)}. Type ${colors.bold().cyan(`npx serve ${dest}`)} to run the app.`);
|
||||
} catch (err) {
|
||||
console.error(colors.bold.red(`> ${err.message}`));
|
||||
console.error(colors.bold().red(`> ${err.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// TODO upgrade
|
||||
|
||||
prog.parse(process.argv);
|
||||
|
||||
function elapsed(start: number) {
|
||||
return prettyMs(Date.now() - start);
|
||||
|
||||
async function _build(
|
||||
bundler: 'rollup' | 'webpack',
|
||||
legacy: boolean,
|
||||
cwd: string,
|
||||
src: string,
|
||||
routes: string,
|
||||
output: string,
|
||||
dest: string
|
||||
) {
|
||||
const { build } = await import('./api/build');
|
||||
|
||||
await build({
|
||||
bundler,
|
||||
legacy,
|
||||
cwd,
|
||||
src,
|
||||
routes,
|
||||
dest,
|
||||
|
||||
oncompile: event => {
|
||||
let banner = `built ${event.type}`;
|
||||
let c = (txt: string) => colors.cyan(txt);
|
||||
|
||||
const { warnings } = event.result;
|
||||
if (warnings.length > 0) {
|
||||
banner += ` with ${warnings.length} ${warnings.length === 1 ? 'warning' : 'warnings'}`;
|
||||
c = (txt: string) => colors.cyan(txt);
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log(c(`┌─${repeat('─', banner.length)}─┐`));
|
||||
console.log(c(`│ ${colors.bold(banner) } │`));
|
||||
console.log(c(`└─${repeat('─', banner.length)}─┘`));
|
||||
|
||||
console.log(event.result.print());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { build as _build } from '../api/build';
|
||||
import colors from 'kleur';
|
||||
import { locations } from '../config';
|
||||
import validate_bundler from './utils/validate_bundler';
|
||||
import { repeat } from '../utils';
|
||||
|
||||
export function build(opts: { bundler?: string, legacy?: boolean }) {
|
||||
const bundler = validate_bundler(opts.bundler);
|
||||
|
||||
if (opts.legacy && bundler === 'webpack') {
|
||||
throw new Error(`Legacy builds are not supported for projects using webpack`);
|
||||
}
|
||||
|
||||
return new Promise((fulfil, reject) => {
|
||||
try {
|
||||
const emitter = _build({
|
||||
legacy: opts.legacy,
|
||||
bundler
|
||||
}, {
|
||||
dest: locations.dest(),
|
||||
src: locations.src(),
|
||||
routes: locations.routes(),
|
||||
webpack: 'webpack',
|
||||
rollup: 'rollup'
|
||||
});
|
||||
|
||||
emitter.on('build', event => {
|
||||
let banner = `built ${event.type}`;
|
||||
let c = colors.cyan;
|
||||
|
||||
const { warnings } = event.result;
|
||||
if (warnings.length > 0) {
|
||||
banner += ` with ${warnings.length} ${warnings.length === 1 ? 'warning' : 'warnings'}`;
|
||||
c = colors.yellow;
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log(c(`┌─${repeat('─', banner.length)}─┐`));
|
||||
console.log(c(`│ ${colors.bold(banner) } │`));
|
||||
console.log(c(`└─${repeat('─', banner.length)}─┘`));
|
||||
|
||||
console.log(event.result.print());
|
||||
});
|
||||
|
||||
emitter.on('error', event => {
|
||||
reject(event.error);
|
||||
});
|
||||
|
||||
emitter.on('done', event => {
|
||||
fulfil();
|
||||
});
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import * as path from 'path';
|
||||
import colors from 'kleur';
|
||||
import * as child_process from 'child_process';
|
||||
import prettyMs from 'pretty-ms';
|
||||
import { dev as _dev } from '../api/dev';
|
||||
import * as events from '../api/interfaces';
|
||||
|
||||
export function dev(opts: { port: number, open: boolean, bundler?: string }) {
|
||||
try {
|
||||
const watcher = _dev(opts);
|
||||
|
||||
let first = true;
|
||||
|
||||
watcher.on('ready', (event: events.ReadyEvent) => {
|
||||
if (first) {
|
||||
console.log(colors.bold.cyan(`> Listening on http://localhost:${event.port}`));
|
||||
if (opts.open) child_process.exec(`open http://localhost:${event.port}`);
|
||||
first = false;
|
||||
}
|
||||
|
||||
// TODO clear screen?
|
||||
|
||||
event.process.stdout.on('data', data => {
|
||||
process.stdout.write(data);
|
||||
});
|
||||
|
||||
event.process.stderr.on('data', data => {
|
||||
process.stderr.write(data);
|
||||
});
|
||||
});
|
||||
|
||||
watcher.on('invalid', (event: events.InvalidEvent) => {
|
||||
const changed = event.changed.map(filename => path.relative(process.cwd(), filename)).join(', ');
|
||||
console.log(`\n${colors.bold.cyan(changed)} changed. rebuilding...`);
|
||||
});
|
||||
|
||||
watcher.on('error', (event: events.ErrorEvent) => {
|
||||
console.log(colors.red(`✗ ${event.type}`));
|
||||
console.log(colors.red(event.message));
|
||||
});
|
||||
|
||||
watcher.on('fatal', (event: events.FatalEvent) => {
|
||||
console.log(colors.bold.red(`> ${event.message}`));
|
||||
if (event.log) console.log(event.log);
|
||||
});
|
||||
|
||||
watcher.on('build', (event: events.BuildEvent) => {
|
||||
if (event.errors.length) {
|
||||
console.log(colors.bold.red(`✗ ${event.type}`));
|
||||
|
||||
event.errors.filter(e => !e.duplicate).forEach(error => {
|
||||
if (error.file) console.log(colors.bold(error.file));
|
||||
console.log(error.message);
|
||||
});
|
||||
|
||||
const hidden = event.errors.filter(e => e.duplicate).length;
|
||||
if (hidden > 0) {
|
||||
console.log(`${hidden} duplicate ${hidden === 1 ? 'error' : 'errors'} hidden\n`);
|
||||
}
|
||||
} else if (event.warnings.length) {
|
||||
console.log(colors.bold.yellow(`• ${event.type}`));
|
||||
|
||||
event.warnings.filter(e => !e.duplicate).forEach(warning => {
|
||||
if (warning.file) console.log(colors.bold(warning.file));
|
||||
console.log(warning.message);
|
||||
});
|
||||
|
||||
const hidden = event.warnings.filter(e => e.duplicate).length;
|
||||
if (hidden > 0) {
|
||||
console.log(`${hidden} duplicate ${hidden === 1 ? 'warning' : 'warnings'} hidden\n`);
|
||||
}
|
||||
} else {
|
||||
console.log(`${colors.bold.green(`✔ ${event.type}`)} ${colors.gray(`(${prettyMs(event.duration)})`)}`);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(colors.bold.red(`> ${err.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import { exporter as _exporter } from '../api/export';
|
||||
import colors from 'kleur';
|
||||
import pb from 'pretty-bytes';
|
||||
import { locations } from '../config';
|
||||
import { left_pad } from '../utils';
|
||||
|
||||
export function exporter(export_dir: string, {
|
||||
basepath = '',
|
||||
timeout
|
||||
}: {
|
||||
basepath: string,
|
||||
timeout: number | false
|
||||
}) {
|
||||
return new Promise((fulfil, reject) => {
|
||||
try {
|
||||
const emitter = _exporter({
|
||||
build: locations.dest(),
|
||||
static: locations.static(),
|
||||
dest: export_dir,
|
||||
basepath,
|
||||
timeout
|
||||
});
|
||||
|
||||
emitter.on('file', event => {
|
||||
const size_color = event.size > 150000 ? colors.bold.red : event.size > 50000 ? colors.bold.yellow : colors.bold.gray;
|
||||
const size_label = size_color(left_pad(pb(event.size), 10));
|
||||
|
||||
const file_label = event.status === 200
|
||||
? event.file
|
||||
: colors.bold[event.status >= 400 ? 'red' : 'yellow'](`(${event.status}) ${event.file}`);
|
||||
|
||||
console.log(`${size_label} ${file_label}`);
|
||||
});
|
||||
|
||||
emitter.on('info', event => {
|
||||
console.log(colors.bold.cyan(`> ${event.message}`));
|
||||
});
|
||||
|
||||
emitter.on('error', event => {
|
||||
reject(event.error);
|
||||
});
|
||||
|
||||
emitter.on('done', event => {
|
||||
fulfil();
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(`${colors.bold.red(`> ${err.message}`)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as child_process from 'child_process';
|
||||
import colors from 'kleur';
|
||||
import * as ports from 'port-authority';
|
||||
|
||||
export async function start(dir: string, opts: { port: number, open: boolean }) {
|
||||
let port = opts.port || +process.env.PORT;
|
||||
|
||||
const resolved = path.resolve(dir);
|
||||
const server = path.resolve(dir, 'server.js');
|
||||
|
||||
if (!fs.existsSync(server)) {
|
||||
console.log(`${colors.bold.red(`> ${dir}/server.js does not exist — type ${colors.bold.cyan(dir === 'build' ? `npx sapper build` : `npx sapper build ${dir}`)} to create it`)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (port) {
|
||||
if (!await ports.check(port)) {
|
||||
console.log(`${colors.bold.red(`> Port ${port} is unavailable`)}`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
port = await ports.find(3000);
|
||||
}
|
||||
|
||||
child_process.fork(server, [], {
|
||||
cwd: process.cwd(),
|
||||
env: Object.assign({
|
||||
NODE_ENV: 'production',
|
||||
PORT: port,
|
||||
SAPPER_DEST: dir
|
||||
}, process.env)
|
||||
});
|
||||
|
||||
await ports.wait(port);
|
||||
console.log(`${colors.bold.cyan(`> Listening on http://localhost:${port}`)}`);
|
||||
if (opts.open) child_process.exec(`open http://localhost:${port}`);
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
import colors from 'kleur';
|
||||
|
||||
export default async function upgrade() {
|
||||
const upgraded = [
|
||||
await upgrade_sapper_main()
|
||||
].filter(Boolean);
|
||||
|
||||
if (upgraded.length === 0) {
|
||||
console.log(`No changes!`);
|
||||
}
|
||||
}
|
||||
|
||||
async function upgrade_sapper_main() {
|
||||
const _2xx = read('templates/2xx.html');
|
||||
const _4xx = read('templates/4xx.html');
|
||||
const _5xx = read('templates/5xx.html');
|
||||
|
||||
const pattern = /<script src='\%sapper\.main\%'><\/script>/;
|
||||
|
||||
let replaced = false;
|
||||
|
||||
['2xx', '4xx', '5xx'].forEach(code => {
|
||||
const file = `templates/${code}.html`
|
||||
const template = read(file);
|
||||
if (!template) return;
|
||||
|
||||
if (/\%sapper\.main\%/.test(template)) {
|
||||
if (!pattern.test(template)) {
|
||||
console.log(`${colors.red(`Could not replace %sapper.main% in ${file}`)}`);
|
||||
} else {
|
||||
write(file, template.replace(pattern, `%sapper.scripts%`));
|
||||
console.log(`${colors.green(`Replaced %sapper.main% in ${file}`)}`);
|
||||
replaced = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return replaced;
|
||||
}
|
||||
|
||||
function read(file: string) {
|
||||
try {
|
||||
return fs.readFileSync(file, 'utf-8');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function write(file: string, data: string) {
|
||||
fs.writeFileSync(file, data);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import * as path from 'path';
|
||||
|
||||
export const dev = () => process.env.NODE_ENV !== 'production';
|
||||
|
||||
export const locations = {
|
||||
base: () => path.resolve(process.env.SAPPER_BASE || ''),
|
||||
src: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_SRC || 'src'),
|
||||
static: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_STATIC || 'static'),
|
||||
routes: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_ROUTES || 'src/routes'),
|
||||
dest: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_DEST || `__sapper__/${dev() ? 'dev' : 'build'}`)
|
||||
};
|
||||
7
src/config/env.ts
Normal file
7
src/config/env.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export let dev: boolean;
|
||||
export let src: string;
|
||||
export let dest: string;
|
||||
|
||||
export const set_dev = (_: boolean) => dev = _;
|
||||
export const set_src = (_: string) => src = _;
|
||||
export const set_dest = (_: string) => dest = _;
|
||||
@@ -1,15 +1,15 @@
|
||||
import { locations, dev } from './config';
|
||||
import { dev, src, dest } from './env';
|
||||
|
||||
export default {
|
||||
dev: dev(),
|
||||
dev,
|
||||
|
||||
client: {
|
||||
input: () => {
|
||||
return `${locations.src()}/client.js`
|
||||
return `${src}/client.js`
|
||||
},
|
||||
|
||||
output: () => {
|
||||
let dir = `${locations.dest()}/client`;
|
||||
let dir = `${dest}/client`;
|
||||
if (process.env.SAPPER_LEGACY_BUILD) dir += `/legacy`;
|
||||
|
||||
return {
|
||||
@@ -17,7 +17,7 @@ export default {
|
||||
entryFileNames: '[name].[hash].js',
|
||||
chunkFileNames: '[name].[hash].js',
|
||||
format: 'esm',
|
||||
sourcemap: dev()
|
||||
sourcemap: dev
|
||||
};
|
||||
}
|
||||
},
|
||||
@@ -25,27 +25,27 @@ export default {
|
||||
server: {
|
||||
input: () => {
|
||||
return {
|
||||
server: `${locations.src()}/server.js`
|
||||
server: `${src}/server.js`
|
||||
};
|
||||
},
|
||||
|
||||
output: () => {
|
||||
return {
|
||||
dir: `${locations.dest()}/server`,
|
||||
dir: `${dest}/server`,
|
||||
format: 'cjs',
|
||||
sourcemap: dev()
|
||||
sourcemap: dev
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
serviceworker: {
|
||||
input: () => {
|
||||
return `${locations.src()}/service-worker.js`;
|
||||
return `${src}/service-worker.js`;
|
||||
},
|
||||
|
||||
output: () => {
|
||||
return {
|
||||
file: `${locations.dest()}/service-worker.js`,
|
||||
file: `${dest}/service-worker.js`,
|
||||
format: 'iife'
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
import { locations, dev } from './config';
|
||||
import { dev, src, dest } from './env';
|
||||
|
||||
export default {
|
||||
dev: dev(),
|
||||
dev,
|
||||
|
||||
client: {
|
||||
entry: () => {
|
||||
return {
|
||||
main: `${locations.src()}/client`
|
||||
main: `${src}/client`
|
||||
};
|
||||
},
|
||||
|
||||
output: () => {
|
||||
return {
|
||||
path: `${locations.dest()}/client`,
|
||||
path: `${dest}/client`,
|
||||
filename: '[hash]/[name].js',
|
||||
chunkFilename: '[hash]/[name].[id].js',
|
||||
publicPath: `client/`
|
||||
@@ -23,13 +23,13 @@ export default {
|
||||
server: {
|
||||
entry: () => {
|
||||
return {
|
||||
server: `${locations.src()}/server`
|
||||
server: `${src}/server`
|
||||
};
|
||||
},
|
||||
|
||||
output: () => {
|
||||
return {
|
||||
path: `${locations.dest()}/server`,
|
||||
path: `${dest}/server`,
|
||||
filename: '[name].js',
|
||||
chunkFilename: '[hash]/[name].[id].js',
|
||||
libraryTarget: 'commonjs2'
|
||||
@@ -40,13 +40,13 @@ export default {
|
||||
serviceworker: {
|
||||
entry: () => {
|
||||
return {
|
||||
'service-worker': `${locations.src()}/service-worker`
|
||||
'service-worker': `${src}/service-worker`
|
||||
};
|
||||
},
|
||||
|
||||
output: () => {
|
||||
return {
|
||||
path: locations.dest(),
|
||||
path: dest,
|
||||
filename: '[name].js',
|
||||
chunkFilename: '[name].[id].[hash].js'
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from './core/create_manifests';
|
||||
export * from './core/create_app';
|
||||
export { default as create_compilers } from './core/create_compilers/index';
|
||||
export { default as create_manifest_data } from './core/create_manifest_data';
|
||||
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}`);
|
||||
}
|
||||
@@ -135,19 +135,21 @@ export default class RollupCompiler {
|
||||
});
|
||||
}
|
||||
|
||||
static async load_config() {
|
||||
if (!rollup) rollup = relative('rollup', process.cwd());
|
||||
static async load_config(cwd: string) {
|
||||
if (!rollup) rollup = relative('rollup', cwd);
|
||||
|
||||
const input = path.resolve('rollup.config.js');
|
||||
const input = path.resolve(cwd, 'rollup.config.js');
|
||||
|
||||
const bundle = await rollup.rollup({
|
||||
input,
|
||||
inlineDynamicImports: true,
|
||||
external: (id: string) => {
|
||||
return (id[0] !== '.' && !path.isAbsolute(id)) || id.slice(-5, id.length) === '.json';
|
||||
}
|
||||
});
|
||||
|
||||
const { code } = await bundle.generate({ format: 'cjs' });
|
||||
const resp = await bundle.generate({ format: 'cjs' });
|
||||
const { code } = resp.output ? resp.output[0] : resp;
|
||||
|
||||
// temporarily override require
|
||||
const defaultLoader = require.extensions['.js'];
|
||||
|
||||
@@ -5,7 +5,7 @@ import RollupCompiler from './RollupCompiler';
|
||||
import extract_css from './extract_css';
|
||||
import { left_pad } from '../../utils';
|
||||
import { CompileResult, BuildInfo, CompileError, Chunk, CssFile } from './interfaces';
|
||||
import { ManifestData, Dirs, PageComponent } from '../../interfaces';
|
||||
import { ManifestData, Dirs } from '../../interfaces';
|
||||
|
||||
export default class RollupResult implements CompileResult {
|
||||
duration: number;
|
||||
@@ -47,12 +47,13 @@ export default class RollupResult implements CompileResult {
|
||||
} else {
|
||||
for (const name in compiler.input) {
|
||||
const file = compiler.input[name];
|
||||
this.assets[name] = compiler.chunks.find(chunk => file in chunk.modules).fileName;
|
||||
const chunk = compiler.chunks.find(chunk => file in chunk.modules);
|
||||
if (chunk) this.assets[name] = chunk.fileName;
|
||||
}
|
||||
}
|
||||
|
||||
this.summary = compiler.chunks.map(chunk => {
|
||||
const size_color = chunk.code.length > 150000 ? colors.bold.red : chunk.code.length > 50000 ? colors.bold.yellow : colors.bold.white;
|
||||
const size_color = chunk.code.length > 150000 ? colors.bold().red : chunk.code.length > 50000 ? colors.bold().yellow : colors.bold().white;
|
||||
const size_label = left_pad(pb(chunk.code.length), 10);
|
||||
|
||||
const lines = [size_color(`${size_label} ${chunk.fileName}`)];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import format_messages from 'webpack-format-messages';
|
||||
import { CompileResult, BuildInfo, CompileError, Chunk, CssFile } from './interfaces';
|
||||
import { ManifestData, Dirs } from '../../interfaces';
|
||||
import { ManifestData, Dirs, PageComponent } from '../../interfaces';
|
||||
|
||||
const locPattern = /\((\d+):(\d+)\)$/;
|
||||
|
||||
@@ -55,14 +55,28 @@ export default class WebpackResult implements CompileResult {
|
||||
}
|
||||
|
||||
to_json(manifest_data: ManifestData, dirs: Dirs): BuildInfo {
|
||||
const extract_css = (assets: string[] | string) => {
|
||||
assets = Array.isArray(assets) ? assets : [assets];
|
||||
return assets.find(asset => /\.css$/.test(asset));
|
||||
};
|
||||
|
||||
return {
|
||||
bundler: 'webpack',
|
||||
shimport: null, // webpack has its own loader
|
||||
assets: this.assets,
|
||||
css: {
|
||||
// TODO
|
||||
main: null,
|
||||
chunks: {}
|
||||
main: extract_css(this.assets.main),
|
||||
chunks: manifest_data.components
|
||||
.reduce((chunks: Record<string, string[]>, component: PageComponent) => {
|
||||
const css_dependencies = [];
|
||||
const css = extract_css(this.assets[component.name]);
|
||||
|
||||
if (css) css_dependencies.push(css);
|
||||
|
||||
chunks[component.file] = css_dependencies;
|
||||
|
||||
return chunks;
|
||||
}, {})
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -70,4 +84,4 @@ export default class WebpackResult implements CompileResult {
|
||||
print() {
|
||||
return this.stats.toString({ colors: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import * as path from 'path';
|
||||
import hash from 'string-hash';
|
||||
import * as codec from 'sourcemap-codec';
|
||||
import { PageComponent, Dirs } from '../../interfaces';
|
||||
import { CompileResult } from './interfaces';
|
||||
import { posixify } from '../utils'
|
||||
import { CompileResult, Chunk } from './interfaces';
|
||||
import { posixify } from '../../utils'
|
||||
|
||||
const inline_sourcemap_header = 'data:application/json;charset=utf-8;base64,';
|
||||
|
||||
@@ -46,6 +46,65 @@ type SourceMap = {
|
||||
mappings: string;
|
||||
};
|
||||
|
||||
function get_css_from_modules(modules: string[], css_map: Map<string, string>, dirs: Dirs) {
|
||||
const parts: string[] = [];
|
||||
const mappings: number[][][] = [];
|
||||
|
||||
const combined_map: SourceMap = {
|
||||
version: 3,
|
||||
file: null,
|
||||
sources: [],
|
||||
sourcesContent: [],
|
||||
names: [],
|
||||
mappings: null
|
||||
};
|
||||
|
||||
modules.forEach(module => {
|
||||
if (!/\.css$/.test(module)) return;
|
||||
|
||||
const css = css_map.get(module);
|
||||
|
||||
const { code, map } = extract_sourcemap(css, module);
|
||||
|
||||
parts.push(code);
|
||||
|
||||
if (map) {
|
||||
const lines = codec.decode(map.mappings);
|
||||
|
||||
if (combined_map.sources.length > 0 || combined_map.names.length > 0) {
|
||||
lines.forEach(line => {
|
||||
line.forEach(segment => {
|
||||
// adjust source index
|
||||
segment[1] += combined_map.sources.length;
|
||||
|
||||
// adjust name index
|
||||
if (segment[4]) segment[4] += combined_map.names.length;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
combined_map.sources.push(...map.sources);
|
||||
combined_map.sourcesContent.push(...map.sourcesContent);
|
||||
combined_map.names.push(...map.names);
|
||||
|
||||
mappings.push(...lines);
|
||||
}
|
||||
});
|
||||
|
||||
if (parts.length > 0) {
|
||||
combined_map.mappings = codec.encode(mappings);
|
||||
|
||||
combined_map.sources = combined_map.sources.map(source => path.relative(`${dirs.dest}/client`, source));
|
||||
|
||||
return {
|
||||
code: parts.join('\n'),
|
||||
map: combined_map
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function extract_css(client_result: CompileResult, components: PageComponent[], dirs: Dirs) {
|
||||
const result: {
|
||||
main: string | null;
|
||||
@@ -57,161 +116,118 @@ export default function extract_css(client_result: CompileResult, components: Pa
|
||||
|
||||
if (!client_result.css_files) return; // Rollup-only for now
|
||||
|
||||
const unaccounted_for = new Set();
|
||||
let asset_dir = `${dirs.dest}/client`;
|
||||
if (process.env.SAPPER_LEGACY_BUILD) asset_dir += '/legacy';
|
||||
|
||||
const css_map = new Map();
|
||||
client_result.css_files.forEach(css => {
|
||||
unaccounted_for.add(css.id);
|
||||
css_map.set(css.id, css.code);
|
||||
const unclaimed = new Set(client_result.css_files.map(x => x.id));
|
||||
|
||||
const lookup = new Map();
|
||||
client_result.chunks.forEach(chunk => {
|
||||
lookup.set(chunk.file, chunk);
|
||||
});
|
||||
|
||||
const chunk_map = new Map();
|
||||
client_result.chunks.forEach(chunk => {
|
||||
chunk_map.set(chunk.file, chunk);
|
||||
const css_map = new Map();
|
||||
client_result.css_files.forEach(css_module => {
|
||||
css_map.set(css_module.id, css_module.code);
|
||||
});
|
||||
|
||||
const chunks_with_css = new Set();
|
||||
|
||||
// figure out which chunks belong to which components...
|
||||
const component_owners = new Map();
|
||||
// concatenate and emit CSS
|
||||
client_result.chunks.forEach(chunk => {
|
||||
chunk.modules.forEach(module => {
|
||||
const component = posixify(path.relative(dirs.routes, module));
|
||||
component_owners.set(component, chunk);
|
||||
});
|
||||
const css_modules = chunk.modules.filter(m => css_map.has(m));
|
||||
if (!css_modules.length) return;
|
||||
|
||||
const css = get_css_from_modules(css_modules, css_map, dirs);
|
||||
|
||||
const { code, map } = css;
|
||||
|
||||
const output_file_name = chunk.file.replace(/\.js$/, '.css');
|
||||
|
||||
map.file = output_file_name;
|
||||
map.sources = map.sources.map(source => path.relative(`${asset_dir}`, source));
|
||||
|
||||
fs.writeFileSync(`${asset_dir}/${output_file_name}`, `${code}\n/* sourceMappingURL=./${output_file_name}.map */`);
|
||||
fs.writeFileSync(`${asset_dir}/${output_file_name}.map`, JSON.stringify(map, null, ' '));
|
||||
|
||||
chunks_with_css.add(chunk);
|
||||
});
|
||||
|
||||
const chunks_depended_upon_by_component = new Map();
|
||||
const entry = path.resolve(dirs.src, 'client.js');
|
||||
const entry_chunk = client_result.chunks.find(chunk => chunk.modules.indexOf(entry) !== -1);
|
||||
|
||||
// ...so we can figure out which chunks don't belong
|
||||
const entry_chunk_dependencies: Set<Chunk> = new Set([entry_chunk]);
|
||||
const entry_css_modules: string[] = [];
|
||||
|
||||
// recursively find the chunks this component depends on
|
||||
entry_chunk_dependencies.forEach(chunk => {
|
||||
if (!chunk) return; // TODO why does this happen?
|
||||
|
||||
chunk.imports.forEach(file => {
|
||||
entry_chunk_dependencies.add(lookup.get(file));
|
||||
});
|
||||
|
||||
if (chunks_with_css.has(chunk)) {
|
||||
chunk.modules.forEach(file => {
|
||||
unclaimed.delete(file);
|
||||
if (css_map.has(file)) {
|
||||
entry_css_modules.push(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// figure out which (css-having) chunks each component depends on
|
||||
components.forEach(component => {
|
||||
const chunk = component_owners.get(component.file);
|
||||
const resolved = path.resolve(dirs.routes, component.file);
|
||||
const chunk: Chunk = client_result.chunks.find(chunk => chunk.modules.indexOf(resolved) !== -1);
|
||||
|
||||
if (!chunk) {
|
||||
// this should never happen!
|
||||
throw new Error(`Could not find chunk that owns ${component.file}`);
|
||||
return;
|
||||
// throw new Error(`Could not find chunk that owns ${component.file}`);
|
||||
}
|
||||
|
||||
const chunks = new Set([chunk]);
|
||||
chunks.forEach(chunk => {
|
||||
chunk.imports.forEach((file: string) => {
|
||||
const chunk = chunk_map.get(file);
|
||||
if (chunk) chunks.add(chunk);
|
||||
const chunk_dependencies: Set<Chunk> = new Set([chunk]);
|
||||
const css_dependencies: string[] = [];
|
||||
|
||||
// recursively find the chunks this component depends on
|
||||
chunk_dependencies.forEach(chunk => {
|
||||
if (!chunk) return; // TODO why does this happen?
|
||||
|
||||
chunk.imports.forEach(file => {
|
||||
chunk_dependencies.add(lookup.get(file));
|
||||
});
|
||||
});
|
||||
|
||||
chunks.forEach(chunk => {
|
||||
chunk.modules.forEach((module: string) => {
|
||||
unaccounted_for.delete(module);
|
||||
});
|
||||
});
|
||||
if (chunks_with_css.has(chunk)) {
|
||||
css_dependencies.push(chunk.file.replace(/\.js$/, '.css'));
|
||||
|
||||
chunks_depended_upon_by_component.set(
|
||||
component,
|
||||
chunks
|
||||
);
|
||||
});
|
||||
|
||||
function get_css_from_modules(modules: string[]) {
|
||||
const parts: string[] = [];
|
||||
const mappings: number[][][] = [];
|
||||
|
||||
const combined_map: SourceMap = {
|
||||
version: 3,
|
||||
file: null,
|
||||
sources: [],
|
||||
sourcesContent: [],
|
||||
names: [],
|
||||
mappings: null
|
||||
};
|
||||
|
||||
modules.forEach(module => {
|
||||
if (!/\.css$/.test(module)) return;
|
||||
|
||||
const css = css_map.get(module);
|
||||
|
||||
const { code, map } = extract_sourcemap(css, module);
|
||||
|
||||
parts.push(code);
|
||||
|
||||
if (map) {
|
||||
const lines = codec.decode(map.mappings);
|
||||
|
||||
if (combined_map.sources.length > 0 || combined_map.names.length > 0) {
|
||||
lines.forEach(line => {
|
||||
line.forEach(segment => {
|
||||
// adjust source index
|
||||
segment[1] += combined_map.sources.length;
|
||||
|
||||
// adjust name index
|
||||
if (segment[4]) segment[4] += combined_map.names.length;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
combined_map.sources.push(...map.sources);
|
||||
combined_map.sourcesContent.push(...map.sourcesContent);
|
||||
combined_map.names.push(...map.names);
|
||||
|
||||
mappings.push(...lines);
|
||||
chunk.modules.forEach(file => {
|
||||
unclaimed.delete(file);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (parts.length > 0) {
|
||||
combined_map.mappings = codec.encode(mappings);
|
||||
result.chunks[component.file] = css_dependencies;
|
||||
});
|
||||
|
||||
combined_map.sources = combined_map.sources.map(source => path.relative(`${dirs.dest}/client`, source));
|
||||
fs.readdirSync(asset_dir).forEach(file => {
|
||||
if (fs.statSync(`${asset_dir}/${file}`).isDirectory()) return;
|
||||
|
||||
return {
|
||||
code: parts.join('\n'),
|
||||
map: combined_map
|
||||
};
|
||||
}
|
||||
const source = fs.readFileSync(`${asset_dir}/${file}`, 'utf-8');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
let main = client_result.assets.main;
|
||||
if (process.env.SAPPER_LEGACY_BUILD) main = `legacy/${main}`;
|
||||
const entry = fs.readFileSync(`${dirs.dest}/client/${main}`, 'utf-8');
|
||||
|
||||
const replacements = new Map();
|
||||
|
||||
chunks_depended_upon_by_component.forEach((chunks, component) => {
|
||||
const chunks_with_css = Array.from(chunks).filter(chunk => {
|
||||
const css = get_css_from_modules(chunk.modules);
|
||||
|
||||
if (css) {
|
||||
const { code, map } = css;
|
||||
|
||||
const output_file_name = chunk.file.replace(/\.js$/, '.css');
|
||||
|
||||
map.file = output_file_name;
|
||||
map.sources = map.sources.map(source => path.relative(`${dirs.dest}/client`, source));
|
||||
|
||||
fs.writeFileSync(`${dirs.dest}/client/${output_file_name}`, `${code}\n/* sourceMappingURL=client/${output_file_name}.map */`);
|
||||
fs.writeFileSync(`${dirs.dest}/client/${output_file_name}.map`, JSON.stringify(map, null, ' '));
|
||||
|
||||
return true;
|
||||
}
|
||||
const replaced = source.replace(/["']__SAPPER_CSS_PLACEHOLDER:(.+?)__["']/g, (m, route) => {
|
||||
return JSON.stringify(result.chunks[route]);
|
||||
});
|
||||
|
||||
const files = chunks_with_css.map(chunk => chunk.file.replace(/\.js$/, '.css'));
|
||||
|
||||
replacements.set(
|
||||
component.file,
|
||||
files
|
||||
);
|
||||
|
||||
result.chunks[component.file] = files;
|
||||
fs.writeFileSync(`${asset_dir}/${file}`, replaced);
|
||||
});
|
||||
|
||||
const replaced = entry.replace(/["']__SAPPER_CSS_PLACEHOLDER:(.+?)__["']/g, (m, route) => {
|
||||
return JSON.stringify(replacements.get(route));
|
||||
unclaimed.forEach(file => {
|
||||
entry_css_modules.push(file);
|
||||
});
|
||||
|
||||
fs.writeFileSync(`${dirs.dest}/client/${main}`, replaced);
|
||||
|
||||
const leftover = get_css_from_modules(Array.from(unaccounted_for));
|
||||
const leftover = get_css_from_modules(entry_css_modules, css_map, dirs);
|
||||
if (leftover) {
|
||||
const { code, map } = leftover;
|
||||
|
||||
@@ -220,10 +236,10 @@ export default function extract_css(client_result: CompileResult, components: Pa
|
||||
const output_file_name = `main.${main_hash}.css`;
|
||||
|
||||
map.file = output_file_name;
|
||||
map.sources = map.sources.map(source => path.relative(`${dirs.dest}/client`, source));
|
||||
map.sources = map.sources.map(source => path.relative(asset_dir, source));
|
||||
|
||||
fs.writeFileSync(`${dirs.dest}/client/${output_file_name}`, `${code}\n/* sourceMappingURL=client/${output_file_name}.map */`);
|
||||
fs.writeFileSync(`${dirs.dest}/client/${output_file_name}.map`, JSON.stringify(map, null, ' '));
|
||||
fs.writeFileSync(`${asset_dir}/${output_file_name}`, `${code}\n/* sourceMappingURL=client/${output_file_name}.map */`);
|
||||
fs.writeFileSync(`${asset_dir}/${output_file_name}.map`, JSON.stringify(map, null, ' '));
|
||||
|
||||
result.main = output_file_name;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as path from 'path';
|
||||
import RollupCompiler from './RollupCompiler';
|
||||
import { WebpackCompiler } from './WebpackCompiler';
|
||||
import { set_dev, set_src, set_dest } from '../../config/env';
|
||||
|
||||
export type Compiler = RollupCompiler | WebpackCompiler;
|
||||
|
||||
@@ -10,9 +11,19 @@ export type Compilers = {
|
||||
serviceworker?: Compiler;
|
||||
}
|
||||
|
||||
export default async function create_compilers(bundler: 'rollup' | 'webpack'): Promise<Compilers> {
|
||||
export default async function create_compilers(
|
||||
bundler: 'rollup' | 'webpack',
|
||||
cwd: string,
|
||||
src: string,
|
||||
dest: string,
|
||||
dev: boolean
|
||||
): Promise<Compilers> {
|
||||
set_dev(dev);
|
||||
set_src(src);
|
||||
set_dest(dest);
|
||||
|
||||
if (bundler === 'rollup') {
|
||||
const config = await RollupCompiler.load_config();
|
||||
const config = await RollupCompiler.load_config(cwd);
|
||||
validate_config(config, 'rollup');
|
||||
|
||||
normalize_rollup_config(config.client);
|
||||
@@ -30,7 +41,7 @@ export default async function create_compilers(bundler: 'rollup' | 'webpack'): P
|
||||
}
|
||||
|
||||
if (bundler === 'webpack') {
|
||||
const config = require(path.resolve('webpack.config.js'));
|
||||
const config = require(path.resolve(cwd, 'webpack.config.js'));
|
||||
validate_config(config, 'webpack');
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,23 +1,48 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { locations } from '../config';
|
||||
import svelte from 'svelte/compiler';
|
||||
import { Page, PageComponent, ServerRoute, ManifestData } from '../interfaces';
|
||||
import { posixify, reserved_words } from './utils';
|
||||
import { posixify, reserved_words } from '../utils';
|
||||
|
||||
export default function create_manifest_data(cwd = locations.routes()): ManifestData {
|
||||
const component_extensions = ['.svelte', '.html']; // TODO make this configurable (to include e.g. .svelte.md?)
|
||||
|
||||
export default function create_manifest_data(cwd: string): ManifestData {
|
||||
// TODO remove in a future version
|
||||
if (!fs.existsSync(cwd)) {
|
||||
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 pages: Page[] = [];
|
||||
const server_routes: ServerRoute[] = [];
|
||||
|
||||
const default_layout: PageComponent = {
|
||||
default: true,
|
||||
type: '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(
|
||||
@@ -44,7 +69,7 @@ export default function create_manifest_data(cwd = locations.routes()): Manifest
|
||||
|
||||
const parts = get_parts(segment);
|
||||
const is_index = is_dir ? false : basename.startsWith('index.');
|
||||
const is_page = ext === '.html';
|
||||
const is_page = component_extensions.indexOf(ext) !== -1;
|
||||
|
||||
parts.forEach(part => {
|
||||
if (/\]\[/.test(part.content)) {
|
||||
@@ -58,6 +83,7 @@ export default function create_manifest_data(cwd = locations.routes()): Manifest
|
||||
|
||||
return {
|
||||
basename,
|
||||
ext,
|
||||
parts,
|
||||
file: posixify(file),
|
||||
is_dir,
|
||||
@@ -104,11 +130,15 @@ export default function create_manifest_data(cwd = locations.routes()): Manifest
|
||||
params.push(...item.parts.filter(p => p.dynamic).map(p => p.content));
|
||||
|
||||
if (item.is_dir) {
|
||||
const index = path.join(dir, item.basename, '_layout.html');
|
||||
const ext = component_extensions.find((ext: string) => {
|
||||
const index = path.join(dir, item.basename, `_layout${ext}`);
|
||||
return fs.existsSync(index);
|
||||
});
|
||||
|
||||
const component = fs.existsSync(index) && {
|
||||
const component = ext && {
|
||||
name: `${get_slug(item.file)}__layout`,
|
||||
file: `${item.file}/_layout.html`
|
||||
file: `${item.file}/_layout${ext}`,
|
||||
has_preload: has_preload(`${item.file}/_layout${ext}`)
|
||||
};
|
||||
|
||||
if (component) components.push(component);
|
||||
@@ -124,28 +154,26 @@ export default function create_manifest_data(cwd = locations.routes()): Manifest
|
||||
}
|
||||
|
||||
else if (item.is_page) {
|
||||
const is_index = item.basename === `index${item.ext}`;
|
||||
|
||||
const component = {
|
||||
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);
|
||||
if (item.basename === 'index.html') {
|
||||
pages.push({
|
||||
pattern: get_pattern(parent_segments, true),
|
||||
parts
|
||||
});
|
||||
} else {
|
||||
pages.push({
|
||||
pattern: get_pattern(segments, true),
|
||||
parts
|
||||
});
|
||||
}
|
||||
|
||||
const parts = (is_index && stack[stack.length - 1] === null)
|
||||
? stack.slice(0, -1).concat({ component, params })
|
||||
: stack.concat({ component, params })
|
||||
|
||||
const page = {
|
||||
pattern: get_pattern(is_index ? parent_segments : segments, true),
|
||||
parts
|
||||
};
|
||||
|
||||
pages.push(page);
|
||||
}
|
||||
|
||||
else {
|
||||
@@ -159,14 +187,24 @@ export default function create_manifest_data(cwd = locations.routes()): Manifest
|
||||
});
|
||||
}
|
||||
|
||||
const root_file = path.join(cwd, '_layout.html');
|
||||
const root = fs.existsSync(root_file)
|
||||
const root_ext = component_extensions.find(ext => fs.existsSync(path.join(cwd, `_layout${ext}`)));
|
||||
const root = root_ext
|
||||
? {
|
||||
name: 'main',
|
||||
file: '_layout.html'
|
||||
file: `_layout${root_ext}`,
|
||||
has_preload: has_preload(`_layout${root_ext}`)
|
||||
}
|
||||
: default_layout;
|
||||
|
||||
const error_ext = component_extensions.find(ext => fs.existsSync(path.join(cwd, `_error${ext}`)));
|
||||
const error = error_ext
|
||||
? {
|
||||
name: 'error',
|
||||
file: `_error${error_ext}`,
|
||||
has_preload: has_preload(`_error${error_ext}`)
|
||||
}
|
||||
: default_error;
|
||||
|
||||
walk(cwd, [], [], []);
|
||||
|
||||
// check for clashes
|
||||
@@ -197,6 +235,7 @@ export default function create_manifest_data(cwd = locations.routes()): Manifest
|
||||
|
||||
return {
|
||||
root,
|
||||
error,
|
||||
components,
|
||||
pages,
|
||||
server_routes
|
||||
@@ -304,4 +343,4 @@ function get_pattern(segments: Part[][], add_trailing_slash: boolean) {
|
||||
}).join('') +
|
||||
(add_trailing_slash ? '\\\/?$' : '$')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import glob from 'tiny-glob/sync.js';
|
||||
import { posixify, stringify, write_if_changed } from './utils';
|
||||
import { dev, locations } from '../config';
|
||||
import { Page, PageComponent, ServerRoute, ManifestData } from '../interfaces';
|
||||
|
||||
export function create_main_manifests({ bundler, manifest_data, dev_port }: {
|
||||
bundler: string,
|
||||
manifest_data: ManifestData;
|
||||
dev_port?: number;
|
||||
}) {
|
||||
const manifest_dir = '__sapper__';
|
||||
if (!fs.existsSync(manifest_dir)) fs.mkdirSync(manifest_dir);
|
||||
|
||||
const path_to_routes = path.relative(manifest_dir, locations.routes());
|
||||
|
||||
const client_manifest = generate_client(manifest_data, path_to_routes, bundler, dev_port);
|
||||
const server_manifest = generate_server(manifest_data, path_to_routes);
|
||||
|
||||
write_if_changed(
|
||||
`${manifest_dir}/default-layout.html`,
|
||||
`<svelte:component this={child.component} {...child.props}/>`
|
||||
);
|
||||
write_if_changed(`${manifest_dir}/client.js`, client_manifest);
|
||||
write_if_changed(`${manifest_dir}/server.js`, server_manifest);
|
||||
}
|
||||
|
||||
export function create_serviceworker_manifest({ manifest_data, client_files }: {
|
||||
manifest_data: ManifestData;
|
||||
client_files: string[];
|
||||
}) {
|
||||
let files;
|
||||
|
||||
// TODO remove in a future version
|
||||
if (fs.existsSync(locations.static())) {
|
||||
files = glob('**', { cwd: locations.static(), filesOnly: true });
|
||||
} else {
|
||||
if (fs.existsSync('assets')) {
|
||||
throw new Error(`As of Sapper 0.21, the assets/ directory should become static/`);
|
||||
}
|
||||
|
||||
files = [];
|
||||
}
|
||||
|
||||
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(`__sapper__/service-worker.js`, code);
|
||||
}
|
||||
|
||||
function generate_client(
|
||||
manifest_data: ManifestData,
|
||||
path_to_routes: string,
|
||||
bundler: string,
|
||||
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
|
||||
) {
|
||||
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 = path.relative(process.cwd(), locations.dest());
|
||||
const src_dir = path.relative(process.cwd(), locations.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 `./default-layout.html`;
|
||||
}
|
||||
|
||||
return posixify(`${path_to_routes}/${component.file}`);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as fs from 'fs';
|
||||
import { locations } from '../config';
|
||||
|
||||
export default function read_template(dir = locations.src()) {
|
||||
export default function read_template(dir: string) {
|
||||
try {
|
||||
return fs.readFileSync(`${dir}/template.html`, 'utf-8');
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
|
||||
const previous_contents = new Map();
|
||||
|
||||
export function write_if_changed(file: string, code: string) {
|
||||
if (code !== previous_contents.get(file)) {
|
||||
previous_contents.set(file, code);
|
||||
fs.writeFileSync(file, code);
|
||||
fudge_mtime(file);
|
||||
}
|
||||
}
|
||||
|
||||
export function posixify(file: string) {
|
||||
return file.replace(/[/\\]/g, '/');
|
||||
}
|
||||
|
||||
export function stringify(string: string, includeQuotes: boolean = true) {
|
||||
const quoted = JSON.stringify(string);
|
||||
return includeQuotes ? quoted : quoted.slice(1, -1);
|
||||
}
|
||||
|
||||
export function fudge_mtime(file: string) {
|
||||
// need to fudge the mtime so that webpack doesn't go doolally
|
||||
const { atime, mtime } = fs.statSync(file);
|
||||
fs.utimesSync(
|
||||
file,
|
||||
new Date(atime.getTime() - 999999),
|
||||
new Date(mtime.getTime() - 999999)
|
||||
);
|
||||
}
|
||||
|
||||
export const reserved_words = new Set([
|
||||
'arguments',
|
||||
'await',
|
||||
'break',
|
||||
'case',
|
||||
'catch',
|
||||
'class',
|
||||
'const',
|
||||
'continue',
|
||||
'debugger',
|
||||
'default',
|
||||
'delete',
|
||||
'do',
|
||||
'else',
|
||||
'enum',
|
||||
'eval',
|
||||
'export',
|
||||
'extends',
|
||||
'false',
|
||||
'finally',
|
||||
'for',
|
||||
'function',
|
||||
'if',
|
||||
'implements',
|
||||
'import',
|
||||
'in',
|
||||
'instanceof',
|
||||
'interface',
|
||||
'let',
|
||||
'new',
|
||||
'null',
|
||||
'package',
|
||||
'private',
|
||||
'protected',
|
||||
'public',
|
||||
'return',
|
||||
'static',
|
||||
'super',
|
||||
'switch',
|
||||
'this',
|
||||
'throw',
|
||||
'true',
|
||||
'try',
|
||||
'typeof',
|
||||
'var',
|
||||
'void',
|
||||
'while',
|
||||
'with',
|
||||
'yield',
|
||||
]);
|
||||
@@ -1,3 +1,6 @@
|
||||
import * as child_process from 'child_process';
|
||||
import { CompileResult } from './core/create_compilers/interfaces';
|
||||
|
||||
export type Route = {
|
||||
id: string;
|
||||
handlers: {
|
||||
@@ -16,14 +19,18 @@ export type Template = {
|
||||
stream: (req, res, data: Record<string, string | Promise<string>>) => void;
|
||||
};
|
||||
|
||||
export type Store = {
|
||||
get: () => any;
|
||||
export type WritableStore<T> = {
|
||||
set: (value: T) => void;
|
||||
update: (fn: (value: T) => T) => void;
|
||||
subscribe: (fn: (T: any) => void) => () => void;
|
||||
};
|
||||
|
||||
export type PageComponent = {
|
||||
default?: boolean;
|
||||
type?: string;
|
||||
name: string;
|
||||
file: string;
|
||||
has_preload: boolean;
|
||||
};
|
||||
|
||||
export type Page = {
|
||||
@@ -44,14 +51,55 @@ export type ServerRoute = {
|
||||
export type Dirs = {
|
||||
dest: string,
|
||||
src: string,
|
||||
routes: string,
|
||||
webpack: string,
|
||||
rollup: string
|
||||
routes: string
|
||||
};
|
||||
|
||||
export type ManifestData = {
|
||||
root: PageComponent;
|
||||
error: PageComponent;
|
||||
components: PageComponent[];
|
||||
pages: Page[];
|
||||
server_routes: ServerRoute[];
|
||||
};
|
||||
};
|
||||
|
||||
export type ReadyEvent = {
|
||||
port: number;
|
||||
process: child_process.ChildProcess;
|
||||
};
|
||||
|
||||
export type ErrorEvent = {
|
||||
type: string;
|
||||
error: Error;
|
||||
};
|
||||
|
||||
export type FatalEvent = {
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type InvalidEvent = {
|
||||
changed: string[];
|
||||
invalid: {
|
||||
client: boolean;
|
||||
server: boolean;
|
||||
serviceworker: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export type BuildEvent = {
|
||||
type: string;
|
||||
errors: Array<{ file: string, message: string, duplicate: boolean }>;
|
||||
warnings: Array<{ file: string, message: string, duplicate: boolean }>;
|
||||
duration: number;
|
||||
result: CompileResult;
|
||||
};
|
||||
|
||||
export type FileEvent = {
|
||||
file: string;
|
||||
size: number;
|
||||
};
|
||||
|
||||
export type FailureEvent = {
|
||||
|
||||
};
|
||||
|
||||
export type DoneEvent = {};
|
||||
1
src/types.d.ts
vendored
Normal file
1
src/types.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare module 'svelte/compiler';
|
||||
111
src/utils.ts
111
src/utils.ts
@@ -1,3 +1,6 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export function left_pad(str: string, len: number) {
|
||||
while (str.length < len) str = ` ${str}`;
|
||||
return str;
|
||||
@@ -7,4 +10,110 @@ export function repeat(str: string, i: number) {
|
||||
let result = '';
|
||||
while (i--) result += str;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export function format_milliseconds(ms: number) {
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
||||
|
||||
const minutes = ~~(ms / 60000);
|
||||
const seconds = Math.round((ms % 60000) / 1000);
|
||||
return `${minutes}m${seconds < 10 ? '0' : ''}${seconds}s`;
|
||||
}
|
||||
|
||||
export function elapsed(start: number) {
|
||||
return format_milliseconds(Date.now() - start);
|
||||
}
|
||||
|
||||
export function walk(cwd: string, dir = cwd, files: string[] = []) {
|
||||
fs.readdirSync(dir).forEach(file => {
|
||||
const resolved = path.resolve(dir, file);
|
||||
if (fs.statSync(resolved).isDirectory()) {
|
||||
walk(cwd, resolved, files);
|
||||
} else {
|
||||
files.push(posixify(path.relative(cwd, resolved)));
|
||||
}
|
||||
});
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
export function posixify(str: string) {
|
||||
return str.replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
const previous_contents = new Map();
|
||||
|
||||
export function write_if_changed(file: string, code: string) {
|
||||
if (code !== previous_contents.get(file)) {
|
||||
previous_contents.set(file, code);
|
||||
fs.writeFileSync(file, code);
|
||||
fudge_mtime(file);
|
||||
}
|
||||
}
|
||||
|
||||
export function stringify(string: string, includeQuotes: boolean = true) {
|
||||
const quoted = JSON.stringify(string);
|
||||
return includeQuotes ? quoted : quoted.slice(1, -1);
|
||||
}
|
||||
|
||||
export function fudge_mtime(file: string) {
|
||||
// need to fudge the mtime so that webpack doesn't go doolally
|
||||
const { atime, mtime } = fs.statSync(file);
|
||||
fs.utimesSync(
|
||||
file,
|
||||
new Date(atime.getTime() - 999999),
|
||||
new Date(mtime.getTime() - 999999)
|
||||
);
|
||||
}
|
||||
|
||||
export const reserved_words = new Set([
|
||||
'arguments',
|
||||
'await',
|
||||
'break',
|
||||
'case',
|
||||
'catch',
|
||||
'class',
|
||||
'const',
|
||||
'continue',
|
||||
'debugger',
|
||||
'default',
|
||||
'delete',
|
||||
'do',
|
||||
'else',
|
||||
'enum',
|
||||
'eval',
|
||||
'export',
|
||||
'extends',
|
||||
'false',
|
||||
'finally',
|
||||
'for',
|
||||
'function',
|
||||
'if',
|
||||
'implements',
|
||||
'import',
|
||||
'in',
|
||||
'instanceof',
|
||||
'interface',
|
||||
'let',
|
||||
'new',
|
||||
'null',
|
||||
'package',
|
||||
'private',
|
||||
'protected',
|
||||
'public',
|
||||
'return',
|
||||
'static',
|
||||
'super',
|
||||
'switch',
|
||||
'this',
|
||||
'throw',
|
||||
'true',
|
||||
'try',
|
||||
'typeof',
|
||||
'var',
|
||||
'void',
|
||||
'while',
|
||||
'with',
|
||||
'yield',
|
||||
]);
|
||||
@@ -1,372 +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 | true> = {};
|
||||
if (url.search.length > 0) {
|
||||
url.search.slice(1).split('&').forEach(searchParam => {
|
||||
const [, key, value] = /([^=]+)(?:=(.*))?/.exec(searchParam);
|
||||
query[key] = decodeURIComponent((value || '').replace(/\+/g, ' '));
|
||||
});
|
||||
}
|
||||
return { url, path, page, match, query };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function scroll_state() {
|
||||
return {
|
||||
x: scrollX,
|
||||
y: scrollY
|
||||
};
|
||||
}
|
||||
|
||||
export function navigate(target: Target, id: number): Promise<any> {
|
||||
if (id) {
|
||||
// popstate or initial navigation
|
||||
cid = id;
|
||||
} else {
|
||||
// clicked on a link. preserve scroll state
|
||||
scroll_history[cid] = scroll_state();
|
||||
|
||||
id = cid = ++uid;
|
||||
scroll_history[cid] = { 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], token);
|
||||
if (document.activeElement) document.activeElement.blur();
|
||||
});
|
||||
}
|
||||
|
||||
function render(data: any, nullable_depth: number, scroll: ScrollPosition, 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 (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 };
|
||||
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 = `${initial_data.baseUrl}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,10 +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 && (!prefetching || href !== prefetching.href)) {
|
||||
set_prefetching(href, prepare_page(target));
|
||||
}
|
||||
}
|
||||
@@ -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__';
|
||||
7
test/app/.gitignore
vendored
7
test/app/.gitignore
vendored
@@ -1,7 +0,0 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
.sapper
|
||||
yarn.lock
|
||||
cypress/screenshots
|
||||
templates/.*
|
||||
dist
|
||||
@@ -1,81 +0,0 @@
|
||||
# sapper-template
|
||||
|
||||
The default [Sapper](https://github.com/sveltejs/sapper) template. To clone it and get started:
|
||||
|
||||
```bash
|
||||
npx degit sveltejs/sapper-template my-app
|
||||
cd my-app
|
||||
npm install # or yarn!
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open up [localhost:3000](http://localhost:3000) and start clicking around.
|
||||
|
||||
|
||||
## Structure
|
||||
|
||||
Sapper expects to find three directories in the root of your project — `assets`, `routes` and `templates`.
|
||||
|
||||
|
||||
### assets
|
||||
|
||||
The [assets](assets) directory contains any static assets that should be available. These are served using [serve-static](https://github.com/expressjs/serve-static).
|
||||
|
||||
In your [service-worker.js](templates/service-worker.js) file, Sapper makes these files available as `__assets__` so that you can cache them (though you can choose not to, for example if you don't want to cache very large files).
|
||||
|
||||
|
||||
### routes
|
||||
|
||||
This is the heart of your Sapper app. There are two kinds of routes — *pages*, and *server routes*.
|
||||
|
||||
**Pages** are Svelte components written in `.html` files. When a user first visits the application, they will be served a server-rendered version of the route in question, plus some JavaScript that 'hydrates' the page and initialises a client-side router. From that point forward, navigating to other pages is handled entirely on the client for a fast, app-like feel. (Sapper will preload and cache the code for these subsequent pages, so that navigation is instantaneous.)
|
||||
|
||||
**Server routes** are modules written in `.js` files, that export functions corresponding to HTTP methods. Each function receives Express `request` and `response` objects as arguments, plus a `next` function. This is useful for creating a JSON API, for example.
|
||||
|
||||
There are three simple rules for naming the files that define your routes:
|
||||
|
||||
* A file called `routes/about.html` corresponds to the `/about` route. A file called `routes/blog/[slug].html` corresponds to the `/blog/:slug` route, in which case `params.slug` is available to the route
|
||||
* The file `routes/index.html` (or `routes/index.js`) corresponds to the root of your app. `routes/about/index.html` is treated the same as `routes/about.html`.
|
||||
* Files and directories with a leading underscore do *not* create routes. This allows you to colocate helper modules and components with the routes that depend on them — for example you could have a file called `routes/_helpers/datetime.js` and it would *not* create a `/_helpers/datetime` route
|
||||
|
||||
|
||||
### templates
|
||||
|
||||
This directory should contain the following files at a minimum:
|
||||
|
||||
* [2xx.html](templates/2xx.html) — a template for the page to serve for valid requests
|
||||
* [4xx.html](templates/4xx.html) — a template for 4xx-range errors (such as 404 Not Found)
|
||||
* [5xx.html](templates/5xx.html) — a template for 5xx-range errors (such as 500 Internal Server Error)
|
||||
* [main.js](templates/main.js) — this module initialises Sapper
|
||||
* [service-worker.js](templates/service-worker.js) — your app's service worker
|
||||
|
||||
Inside the HTML templates, Sapper will inject various values as indicated by `%sapper.xxxx%` tags. Inside JavaScript files, Sapper will replace strings like `__dev__` with the appropriate value.
|
||||
|
||||
In lieu of documentation (bear with us), consult the files to see what variables are available and how they're used.
|
||||
|
||||
|
||||
## Webpack config
|
||||
|
||||
Sapper uses webpack to provide code-splitting, dynamic imports and hot module reloading, as well as compiling your Svelte components. As long as you don't do anything daft, you can edit the configuration files to add whatever loaders and plugins you'd like.
|
||||
|
||||
|
||||
## Production mode and deployment
|
||||
|
||||
To start a production version of your app, run `npm start`. This will disable hot module replacement, and activate the appropriate webpack plugins.
|
||||
|
||||
You can deploy your application to any environment that supports Node 8 or above. As an example, to deploy to [Now](https://zeit.co/now), run these commands:
|
||||
|
||||
```bash
|
||||
npm install -g now
|
||||
now
|
||||
```
|
||||
|
||||
|
||||
## Bugs and feedback
|
||||
|
||||
Sapper is in early development, and may have the odd rough edge here and there. Please be vocal over on the [Sapper issue tracker](https://github.com/sveltejs/sapper/issues).
|
||||
|
||||
|
||||
## License
|
||||
|
||||
[LIL](LICENSE)
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Store } from 'svelte/store.js';
|
||||
import * as sapper from '../__sapper__/client.js';
|
||||
|
||||
window.init = () => {
|
||||
return sapper.start({
|
||||
target: document.querySelector('#sapper'),
|
||||
store: data => new Store(data)
|
||||
});
|
||||
};
|
||||
|
||||
window.prefetchRoutes = sapper.prefetchRoutes;
|
||||
window.goto = sapper.goto;
|
||||
@@ -1,20 +0,0 @@
|
||||
<span>z: {segment} {count}</span>
|
||||
<a href="foo/bar/qux"></a>
|
||||
|
||||
<script>
|
||||
import counts from '../_counts.js';
|
||||
|
||||
export default {
|
||||
preload() {
|
||||
return {
|
||||
count: counts.z += 1
|
||||
};
|
||||
},
|
||||
|
||||
oncreate() {
|
||||
this.set({
|
||||
segment: this.get().params.z
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,22 +0,0 @@
|
||||
<span>y: {segment} {count}</span>
|
||||
<svelte:component this={child.component} {...child.props}/>
|
||||
|
||||
<span>child segment: {child.segment}</span>
|
||||
|
||||
<script>
|
||||
import counts from '../_counts.js';
|
||||
|
||||
export default {
|
||||
preload() {
|
||||
return {
|
||||
count: counts.y += 1
|
||||
};
|
||||
},
|
||||
|
||||
oncreate() {
|
||||
this.set({
|
||||
segment: this.get().params.y
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<svelte:head>
|
||||
<title>{status}</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1>{status}</h1>
|
||||
<p>{error.message}</p>
|
||||
@@ -1,15 +0,0 @@
|
||||
{#if preloading}
|
||||
<progress class='preloading-progress' value=0.5/>
|
||||
{/if}
|
||||
|
||||
<svelte:component this={child.component} {rootPreloadFunctionRan} {...child.props}/>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload() {
|
||||
return {
|
||||
rootPreloadFunctionRan: true
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,19 +0,0 @@
|
||||
<svelte:head>
|
||||
<title>About</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1>About this site</h1>
|
||||
|
||||
<p>This is the 'about' page. There's not much here.</p>
|
||||
|
||||
<button class='prefetch' on:click='prefetch("blog/why-the-name")'>Why the name?</button>
|
||||
|
||||
<script>
|
||||
import { prefetch } from '../../__sapper__/client.js';
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
prefetch
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,36 +0,0 @@
|
||||
<svelte:head>
|
||||
<title>{post.title}</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1>{post.title}</h1>
|
||||
|
||||
<div class='content'>
|
||||
{@html post.html}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload({ params, query }) {
|
||||
// the `slug` parameter is available because this file
|
||||
// is called [slug].html
|
||||
const { slug } = params;
|
||||
|
||||
if (slug === 'throw-an-error') {
|
||||
return this.error(500, 'something went wrong');
|
||||
}
|
||||
|
||||
return fetch(`blog/${slug}.json`).then(r => {
|
||||
if (r.status === 200) {
|
||||
return r.json().then(post => ({ post }));
|
||||
this.error(r.status, '')
|
||||
}
|
||||
|
||||
if (r.status === 404) {
|
||||
this.error(404, 'Not found');
|
||||
} else {
|
||||
throw new Error('Something went wrong');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,23 +0,0 @@
|
||||
import posts from './_posts.js';
|
||||
|
||||
const lookup = {};
|
||||
posts.forEach(post => {
|
||||
lookup[post.slug] = JSON.stringify(post);
|
||||
});
|
||||
|
||||
export function get(req, res, next) {
|
||||
// the `slug` parameter is available because this file
|
||||
// is called [slug].js
|
||||
const { slug } = req.params;
|
||||
|
||||
if (slug in lookup) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': `no-cache`
|
||||
});
|
||||
|
||||
res.end(lookup[slug]);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
// Ordinarily, you'd generate this data from markdown files in your
|
||||
// repo, or fetch them from a database of some kind. But in order to
|
||||
// avoid unnecessary dependencies in the starter template, and in the
|
||||
// service of obviousness, we're just going to leave it here.
|
||||
|
||||
// This file is called `_posts.js` rather than `posts.js`, because
|
||||
// we don't want to create an `/api/blog/posts` route — the leading
|
||||
// underscore tells Sapper not to do that.
|
||||
|
||||
const posts = [
|
||||
{
|
||||
title: 'What is Sapper?',
|
||||
slug: 'what-is-sapper',
|
||||
html: `
|
||||
<p>First, you have to know what <a href='https://svelte.technology'>Svelte</a> is. Svelte is a UI framework with a bold new idea: rather than providing a library that you write code with (like React or Vue, for example), it's a compiler that turns your components into highly optimized vanilla JavaScript. If you haven't already read the <a href='https://svelte.technology/blog/frameworks-without-the-framework'>introductory blog post</a>, you should!</p>
|
||||
|
||||
<p>Sapper is a Next.js-style framework (<a href='blog/how-is-sapper-different-from-next'>more on that here</a>) built around Svelte. It makes it embarrassingly easy to create extremely high performance web apps. Out of the box, you get:</p>
|
||||
|
||||
<ul>
|
||||
<li>Code-splitting, dynamic imports and hot module replacement, powered by webpack</li>
|
||||
<li>Server-side rendering (SSR) with client-side hydration</li>
|
||||
<li>Service worker for offline support, and all the PWA bells and whistles</li>
|
||||
<li>The nicest development experience you've ever had, or your money back</li>
|
||||
</ul>
|
||||
|
||||
<p>It's implemented as Express middleware. Everything is set up and waiting for you to get started, but you keep complete control over the server, service worker, webpack config and everything else, so it's as flexible as you need it to be.</p>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
title: 'How to use Sapper',
|
||||
slug: 'how-to-use-sapper',
|
||||
html: `
|
||||
<h2>Step one</h2>
|
||||
<p>Create a new project, using <a href='https://github.com/Rich-Harris/degit'>degit</a>:</p>
|
||||
|
||||
<pre><code>npx degit sveltejs/sapper-template my-app
|
||||
cd my-app
|
||||
npm install # or yarn!
|
||||
npm run dev
|
||||
</code></pre>
|
||||
|
||||
<h2>Step two</h2>
|
||||
<p>Go to <a href='http://localhost:3000'>localhost:3000</a>. Open <code>my-app</code> in your editor. Edit the files in the <code>routes</code> directory or add new ones.</p>
|
||||
|
||||
<h2>Step three</h2>
|
||||
<p>...</p>
|
||||
|
||||
<h2>Step four</h2>
|
||||
<p>Resist overdone joke formats.</p>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Why the name?',
|
||||
slug: 'why-the-name',
|
||||
html: `
|
||||
<p>In war, the soldiers who build bridges, repair roads, clear minefields and conduct demolitions — all under combat conditions — are known as <em>sappers</em>.</p>
|
||||
|
||||
<p>For web developers, the stakes are generally lower than those for combat engineers. But we face our own hostile environment: underpowered devices, poor network connections, and the complexity inherent in front-end engineering. Sapper, which is short for <strong>S</strong>velte <strong>app</strong> mak<strong>er</strong>, is your courageous and dutiful ally.</p>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
title: 'How is Sapper different from Next.js?',
|
||||
slug: 'how-is-sapper-different-from-next',
|
||||
html: `
|
||||
<p><a href='https://github.com/zeit/next.js/'>Next.js</a> is a React framework from <a href='https://zeit.co'>Zeit</a>, and is the inspiration for Sapper. There are a few notable differences, however:</p>
|
||||
|
||||
<ul>
|
||||
<li>It's powered by <a href='https://svelte.technology'>Svelte</a> instead of React, so it's faster and your apps are smaller</li>
|
||||
<li>Instead of route masking, we encode route parameters in filenames. For example, the page you're looking at right now is <code>routes/blog/[slug].html</code></li>
|
||||
<li>As well as pages (Svelte components, which render on server or client), you can create <em>server routes</em> in your <code>routes</code> directory. These are just <code>.js</code> files that export functions corresponding to HTTP methods, and receive Express <code>request</code> and <code>response</code> objects as arguments. This makes it very easy to, for example, add a JSON API such as the one <a href='blog/how-is-sapper-different-from-next.json'>powering this very page</a></li>
|
||||
<li>Links are just <code><a></code> elements, rather than framework-specific <code><Link></code> components. That means, for example, that <a href='blog/how-can-i-get-involved'>this link right here</a>, despite being inside a blob of HTML, works with the router as you'd expect.</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
title: 'How can I get involved?',
|
||||
slug: 'how-can-i-get-involved',
|
||||
html: `
|
||||
<p>We're so glad you asked! Come on over to the <a href='https://github.com/sveltejs/svelte'>Svelte</a> and <a href='https://github.com/sveltejs/sapper'>Sapper</a> repos, and join us in the <a href='https://gitter.im/sveltejs/svelte'>Gitter chatroom</a>. Everyone is welcome, especially you!</p>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
title: 'A very long post with deep links',
|
||||
slug: 'a-very-long-post',
|
||||
html: `
|
||||
<h2 id='one'>One</h2>
|
||||
<p>I'll have a vodka rocks. (Mom, it's breakfast time.) And a piece of toast. Let me out that Queen. Fried cheese… with club sauce.</p>
|
||||
<p>Her lawyers are claiming the seal is worth $250,000. And that's not even including Buster's Swatch. This was a big get for God. What, so the guy we are meeting with can't even grow his own hair? COME ON! She's always got to wedge herself in the middle of us so that she can control everything. Yeah. Mom's awesome. It's, like, Hey, you want to go down to the whirlpool? Yeah, I don't have a husband. I call it Swing City. The CIA should've just Googled for his hideout, evidently. There are dozens of us! DOZENS! Yeah, like I'm going to take a whiz through this $5,000 suit. COME ON.</p>
|
||||
|
||||
<h2 id='two'>Two</h2>
|
||||
<p>Tobias Fünke costume. Heart attack never stopped old big bear.</p>
|
||||
<p>Nellie is blowing them all AWAY. I will be a bigger and hairier mole than the one on your inner left thigh! I'll sacrifice anything for my children.</p>
|
||||
<p>Up yours, granny! You couldn't handle it! Hey, Dad. Look at you. You're a year older…and a year closer to death. Buster: Oh yeah, I guess that's kind of funny. Bob Loblaw Law Blog. The guy runs a prison, he can have any piece of ass he wants.</p>
|
||||
|
||||
<h2 id='three'>Three</h2>
|
||||
<p>I prematurely shot my wad on what was supposed to be a dry run, so now I'm afraid I have something of a mess on my hands. Dead Dove DO NOT EAT. Never once touched my per diem. I'd go to Craft Service, get some raw veggies, bacon, Cup-A-Soup…baby, I got a stew goin'. You're losing blood, aren't you? Gob: Probably, my socks are wet. Sure, let the little fruit do it. HUZZAH! Although George Michael had only got to second base, he'd gone in head first, like Pete Rose. I will pack your sweet pink mouth with so much ice cream you'll be the envy of every Jerry and Jane on the block!</p>
|
||||
<p>Gosh Mom… after all these years, God's not going to take a call from you. Come on, this is a Bluth family celebration. It's no place for children.</p>
|
||||
<p>And I wouldn't just lie there, if that's what you're thinking. That's not what I WAS thinking. Who? i just dont want him to point out my cracker ass in front of ann. When a man needs to prove to a woman that he's actually… When a man loves a woman… Heyyyyyy Uncle Father Oscar. [Stabbing Gob] White power! Gob: I'm white! Let me take off my assistant's skirt and put on my Barbra-Streisand-in-The-Prince-of-Tides ass-masking therapist pantsuit. In the mid '90s, Tobias formed a folk music band with Lindsay and Maebe which he called Dr. Funke's 100 Percent Natural Good Time Family Band Solution. The group was underwritten by the Natural Food Life Company, a division of Chem-Grow, an Allen Crayne acqusition, which was part of the Squimm Group. Their motto was simple: We keep you alive.</p>
|
||||
|
||||
<h2 id='four'>Four</h2>
|
||||
<p>If you didn't have adult onset diabetes, I wouldn't mind giving you a little sugar. Everybody dance NOW. And the soup of the day is bread. Great, now I'm gonna smell to high heaven like a tuna melt!</p>
|
||||
<p>That's how Tony Wonder lost a nut. She calls it a Mayonegg. Go ahead, touch the Cornballer. There's a new daddy in town. A discipline daddy.</p>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Encödïng test',
|
||||
slug: 'encödïng-test',
|
||||
html: `
|
||||
<p>It works</p>
|
||||
`
|
||||
}
|
||||
];
|
||||
|
||||
posts.forEach(post => {
|
||||
post.html = post.html.replace(/^\t{3}/gm, '');
|
||||
});
|
||||
|
||||
export default posts;
|
||||
@@ -1,25 +0,0 @@
|
||||
<svelte:head>
|
||||
<title>Blog</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1>Recent posts</h1>
|
||||
|
||||
<ul>
|
||||
{#each posts as post}
|
||||
<!-- we're using the non-standard `rel=prefetch` attribute to
|
||||
tell Sapper to load the data for the page as soon as
|
||||
the user hovers over the link or taps it, instead of
|
||||
waiting for the 'click' event -->
|
||||
<li><a rel='prefetch' href='blog/{post.slug}'>{post.title}</a></li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload({ params, query }) {
|
||||
return fetch(`blog.json`).then(r => r.json()).then(posts => {
|
||||
return { posts };
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,17 +0,0 @@
|
||||
import posts from './_posts.js';
|
||||
|
||||
const contents = JSON.stringify(posts.map(post => {
|
||||
return {
|
||||
title: post.title,
|
||||
slug: post.slug
|
||||
};
|
||||
}));
|
||||
|
||||
export function get(req, res) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': `max-age=${30 * 60 * 1e3}` // cache for 30 minutes
|
||||
});
|
||||
|
||||
res.end(contents);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<h1>{message}</h1>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload({ query }) {
|
||||
return this.fetch(`credentials/test.json`, {
|
||||
credentials: query.creds
|
||||
}).then(r => r.json());
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,28 +0,0 @@
|
||||
export function get(req, res) {
|
||||
const cookies = req.headers.cookie
|
||||
? req.headers.cookie.split(/,\s+/).reduce((cookies, cookie) => {
|
||||
const [pair] = cookie.split('; ');
|
||||
const [name, value] = pair.split('=');
|
||||
cookies[name] = value;
|
||||
return cookies;
|
||||
}, {})
|
||||
: {};
|
||||
|
||||
if (cookies.test) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
|
||||
res.end(JSON.stringify({
|
||||
message: cookies.test
|
||||
}));
|
||||
} else {
|
||||
res.writeHead(403, {
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
|
||||
res.end(JSON.stringify({
|
||||
message: 'unauthorized'
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<button class='del' on:click='del()'>delete</button>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
del() {
|
||||
fetch(`api/delete/42`, { method: 'DELETE' })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
window.deleted = data;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,12 +0,0 @@
|
||||
<h1>{slug} ({message})</h1>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload({ params, query }) {
|
||||
return {
|
||||
slug: params.slug,
|
||||
message: query.message
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,11 +0,0 @@
|
||||
<h1>{phrase}</h1>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload() {
|
||||
return this.fetch('fünke.json').then(r => r.json()).then(phrase => {
|
||||
return { phrase };
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,31 +0,0 @@
|
||||
<svelte:head>
|
||||
<title>Sapper project template</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1>Great success!</h1>
|
||||
|
||||
<a href='.'>home</a>
|
||||
<a href='about'>about</a>
|
||||
<a href='slow-preload'>slow preload</a>
|
||||
<a href='non-sapper-redirect-from'>redirect</a>
|
||||
<a href='redirect-from'>redirect</a>
|
||||
<a href='redirect-root'>redirect (root)</a>
|
||||
<a href='blog/nope'>broken link</a>
|
||||
<a href='blog/throw-an-error'>error link</a>
|
||||
<a href='credentials?creds=include'>credentials</a>
|
||||
<a rel=prefetch class='{page === "blog" ? "selected" : ""}' href='blog'>blog</a>
|
||||
<a href="const">const</a>
|
||||
<a href="echo/page/encöded?message=hëllö+wörld">echo/page/encöded?message=hëllö+wörld</a>
|
||||
<a href="echo/page/empty?message">echo/page/empty?message</a>
|
||||
|
||||
<div class='hydrate-test'></div>
|
||||
|
||||
<style>
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-size: 2.8em;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
margin: 0 0 0.5em 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1 +0,0 @@
|
||||
<h1>it works</h1>
|
||||
@@ -1 +0,0 @@
|
||||
<h1>root preload function ran: {rootPreloadFunctionRan}</h1>
|
||||
@@ -1,17 +0,0 @@
|
||||
<h1>{foo.bar()}</h1>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload() {
|
||||
class Foo {
|
||||
bar() {
|
||||
return 42;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
foo: new Foo()
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1 +0,0 @@
|
||||
<svelte:component this={child.component} {...child.props}/>
|
||||
@@ -1,11 +0,0 @@
|
||||
<h1>{set.has('x')}</h1>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload() {
|
||||
return {
|
||||
set: new Set(['x'])
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,7 +0,0 @@
|
||||
<script>
|
||||
export default {
|
||||
preload() {
|
||||
this.redirect(301, 'redirect-to');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,7 +0,0 @@
|
||||
<script>
|
||||
export default {
|
||||
preload() {
|
||||
this.redirect(301, '/');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1 +0,0 @@
|
||||
<h1>redirected</h1>
|
||||
@@ -1,15 +0,0 @@
|
||||
<h1>This page should never render</h1>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload() {
|
||||
return new Promise(fulfil => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.fulfil = fulfil;
|
||||
} else {
|
||||
fulfil({});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1 +0,0 @@
|
||||
<h1>{$title}</h1>
|
||||
@@ -1,3 +0,0 @@
|
||||
export function get() {
|
||||
throw new Error('nope');
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
$&
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload() {
|
||||
return '$&';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,126 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import { resolve } from 'url';
|
||||
import express from 'express';
|
||||
import serve from 'serve-static';
|
||||
import { Store } from 'svelte/store.js';
|
||||
import * as sapper from '../__sapper__/server.js';
|
||||
|
||||
let pending;
|
||||
let ended;
|
||||
|
||||
process.on('message', message => {
|
||||
if (message.action === 'start') {
|
||||
if (pending) {
|
||||
throw new Error(`Already capturing`);
|
||||
}
|
||||
|
||||
pending = new Set();
|
||||
ended = false;
|
||||
process.send({ type: 'ready' });
|
||||
}
|
||||
|
||||
if (message.action === 'end') {
|
||||
ended = true;
|
||||
if (pending.size === 0) {
|
||||
process.send({ type: 'done' });
|
||||
pending = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const app = express();
|
||||
|
||||
const { PORT = 3000, BASEPATH = '' } = process.env;
|
||||
const base = `http://localhost:${PORT}${BASEPATH}/`;
|
||||
|
||||
// this allows us to do e.g. `fetch('/api/blog')` on the server
|
||||
const fetch = require('node-fetch');
|
||||
global.fetch = (url, opts) => {
|
||||
return fetch(resolve(base, url), opts);
|
||||
};
|
||||
|
||||
const middlewares = [
|
||||
serve('assets'),
|
||||
|
||||
// set test cookie
|
||||
(req, res, next) => {
|
||||
res.setHeader('Set-Cookie', 'test=woohoo!; Max-Age=3600');
|
||||
next();
|
||||
},
|
||||
|
||||
// emit messages so we can capture requests
|
||||
(req, res, next) => {
|
||||
if (!pending) return next();
|
||||
|
||||
pending.add(req.url);
|
||||
|
||||
const { write, end } = res;
|
||||
const chunks = [];
|
||||
|
||||
res.write = function(chunk) {
|
||||
chunks.push(new Buffer(chunk));
|
||||
write.apply(res, arguments);
|
||||
};
|
||||
|
||||
res.end = function(chunk) {
|
||||
if (chunk) chunks.push(new Buffer(chunk));
|
||||
end.apply(res, arguments);
|
||||
|
||||
if (pending) pending.delete(req.url);
|
||||
|
||||
process.send({
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
status: res.statusCode,
|
||||
headers: res._headers,
|
||||
body: Buffer.concat(chunks).toString()
|
||||
});
|
||||
|
||||
if (pending && pending.size === 0 && ended) {
|
||||
process.send({ type: 'done' });
|
||||
}
|
||||
};
|
||||
|
||||
next();
|
||||
},
|
||||
|
||||
// set up some values for the store
|
||||
(req, res, next) => {
|
||||
req.hello = 'hello';
|
||||
res.locals = { name: 'world' };
|
||||
next();
|
||||
},
|
||||
|
||||
sapper.middleware({
|
||||
store: (req, res) => {
|
||||
return new Store({
|
||||
title: `${req.hello} ${res.locals.name}`
|
||||
});
|
||||
},
|
||||
ignore: [
|
||||
/foobar/i,
|
||||
'/buzz',
|
||||
'fizz',
|
||||
x => x === '/hello'
|
||||
]
|
||||
}),
|
||||
];
|
||||
|
||||
app.get(`${BASEPATH}/non-sapper-redirect-from`, (req, res) => {
|
||||
res.writeHead(301, {
|
||||
Location: `${BASEPATH}/non-sapper-redirect-to`
|
||||
});
|
||||
res.end();
|
||||
});
|
||||
|
||||
if (BASEPATH) {
|
||||
app.use(BASEPATH, ...middlewares);
|
||||
} else {
|
||||
app.use(...middlewares);
|
||||
}
|
||||
|
||||
['foobar', 'buzz', 'fizzer', 'hello'].forEach(uri => {
|
||||
app.get('/'+uri, (req, res) => res.end(uri));
|
||||
});
|
||||
|
||||
app.listen(PORT);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user