Compare commits
4 Commits
v0.22.10
...
proxy-data
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b799a3f1e | ||
|
|
18d15c0120 | ||
|
|
b20e15721c | ||
|
|
06cc22b10d |
4
.gitignore
vendored
@@ -4,12 +4,10 @@ yarn-error.log
|
|||||||
node_modules
|
node_modules
|
||||||
cypress/screenshots
|
cypress/screenshots
|
||||||
test/app/.sapper
|
test/app/.sapper
|
||||||
test/app/src/manifest
|
test/app/app/manifest
|
||||||
__sapper__
|
|
||||||
test/app/export
|
test/app/export
|
||||||
test/app/build
|
test/app/build
|
||||||
sapper
|
sapper
|
||||||
runtime.js
|
runtime.js
|
||||||
dist
|
dist
|
||||||
!rollup.config.js
|
!rollup.config.js
|
||||||
templates/*.js
|
|
||||||
@@ -18,4 +18,4 @@ addons:
|
|||||||
install:
|
install:
|
||||||
- export DISPLAY=':99.0'
|
- export DISPLAY=':99.0'
|
||||||
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
||||||
- npm ci || npm i
|
- npm install
|
||||||
|
|||||||
166
CHANGELOG.md
@@ -1,171 +1,5 @@
|
|||||||
# sapper changelog
|
# sapper changelog
|
||||||
|
|
||||||
## 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))
|
|
||||||
* Load modules with credentials ([#458](https://github.com/sveltejs/sapper/pull/458))
|
|
||||||
|
|
||||||
## 0.22.5
|
|
||||||
|
|
||||||
* Fix `sapper dev`. Oops.
|
|
||||||
|
|
||||||
## 0.22.4
|
|
||||||
|
|
||||||
* Ensure launcher does not overwrite a module ([#455](https://github.com/sveltejs/sapper/pull/455))
|
|
||||||
|
|
||||||
## 0.22.3
|
|
||||||
|
|
||||||
* Prevent server from accidentally importing dev client
|
|
||||||
|
|
||||||
## 0.22.2
|
|
||||||
|
|
||||||
* Make paths in generated code relative to project
|
|
||||||
|
|
||||||
## 0.22.1
|
|
||||||
|
|
||||||
* Fix `pkg.files`
|
|
||||||
|
|
||||||
## 0.22.0
|
|
||||||
|
|
||||||
* Move generated files into `__sapper__` ([#453](https://github.com/sveltejs/sapper/pull/453))
|
|
||||||
* Change default build and export directories to `__sapper__/build` and `__sapper__/export` ([#453](https://github.com/sveltejs/sapper/pull/453))
|
|
||||||
|
|
||||||
## 0.21.1
|
|
||||||
|
|
||||||
* Read template from build directory in production
|
|
||||||
|
|
||||||
## 0.21.0
|
|
||||||
|
|
||||||
* Change project folder structure ([#432](https://github.com/sveltejs/sapper/issues/432))
|
|
||||||
* Escape filenames ([#446](https://github.com/sveltejs/sapper/pull/446/))
|
|
||||||
|
|
||||||
## 0.20.4
|
|
||||||
|
|
||||||
* Fix legacy build CSS ([#439](https://github.com/sveltejs/sapper/issues/439))
|
|
||||||
* Enable debugging in Chrome and VSCode ([#435](https://github.com/sveltejs/sapper/issues/435))
|
|
||||||
|
|
||||||
## 0.20.3
|
|
||||||
|
|
||||||
* Inject `nonce` attribute if `res.locals.nonce` is present ([#424](https://github.com/sveltejs/sapper/pull/424))
|
|
||||||
* Prevent service worker caching ([#428](https://github.com/sveltejs/sapper/pull/428))
|
|
||||||
* Consistent caching for HTML responses ([#429](https://github.com/sveltejs/sapper/pull/429))
|
|
||||||
|
|
||||||
## 0.20.2
|
|
||||||
|
|
||||||
* Add `immutable` cache control header for hashed assets ([#425](https://github.com/sveltejs/sapper/pull/425))
|
|
||||||
* Handle value-less query string params ([#426](https://github.com/sveltejs/sapper/issues/426))
|
|
||||||
|
|
||||||
## 0.20.1
|
|
||||||
|
|
||||||
* Update shimport
|
|
||||||
|
|
||||||
## 0.20.0
|
|
||||||
|
|
||||||
* Decode `req.params` and `req.query` ([#417](https://github.com/sveltejs/sapper/issues/417))
|
|
||||||
* Decode URLs before writing files in `sapper export` ([#414](https://github.com/sveltejs/sapper/pull/414))
|
|
||||||
* Generate server sourcemaps for Rollup apps in dev mode ([#418](https://github.com/sveltejs/sapper/pull/418))
|
|
||||||
|
|
||||||
## 0.19.3
|
|
||||||
|
|
||||||
* Better unicode route handling ([#347](https://github.com/sveltejs/sapper/issues/347))
|
|
||||||
|
|
||||||
## 0.19.2
|
|
||||||
|
|
||||||
* Ignore editor tmp files ([#220](https://github.com/sveltejs/sapper/issues/220))
|
|
||||||
* Ignore clicks an `<a>` element without `href` ([#235](https://github.com/sveltejs/sapper/issues/235))
|
|
||||||
* Allow routes that are reserved JavaScript words ([#315](https://github.com/sveltejs/sapper/issues/315))
|
|
||||||
* Print out webpack errors ([#403](https://github.com/sveltejs/sapper/issues/403))
|
|
||||||
|
|
||||||
## 0.19.1
|
|
||||||
|
|
||||||
* Don't include local origin in export redirects ([#409](https://github.com/sveltejs/sapper/pull/409))
|
|
||||||
|
|
||||||
## 0.19.0
|
|
||||||
|
|
||||||
* Extract styles out of JS into .css files, for Rollup apps ([#388](https://github.com/sveltejs/sapper/issues/388))
|
|
||||||
* Fix `prefetchRoutes` ([#380](https://github.com/sveltejs/sapper/issues/380))
|
|
||||||
|
|
||||||
## 0.18.7
|
|
||||||
|
|
||||||
* Support differential bundling for Rollup apps via a `--legacy` flag ([#280](https://github.com/sveltejs/sapper/issues/280))
|
|
||||||
|
|
||||||
## 0.18.6
|
|
||||||
|
|
||||||
* Bundle missing dependency
|
|
||||||
|
|
||||||
## 0.18.5
|
|
||||||
|
|
||||||
* Bugfix
|
|
||||||
|
|
||||||
## 0.18.4
|
|
||||||
|
|
||||||
* Handle non-Sapper responses when exporting ([#382](https://github.com/sveltejs/sapper/issues/392))
|
|
||||||
* Add `--dev-port` flag to `sapper dev` ([#381](https://github.com/sveltejs/sapper/issues/381))
|
|
||||||
|
|
||||||
## 0.18.3
|
|
||||||
|
|
||||||
* Fix service worker Rollup build config
|
|
||||||
|
|
||||||
## 0.18.2
|
|
||||||
|
|
||||||
* Update `pkg.files`
|
|
||||||
|
|
||||||
## 0.18.1
|
|
||||||
|
|
||||||
* Add live reloading ([#385](https://github.com/sveltejs/sapper/issues/385))
|
|
||||||
|
|
||||||
## 0.18.0
|
|
||||||
|
|
||||||
* Rollup support ([#379](https://github.com/sveltejs/sapper/pull/379))
|
|
||||||
* Fail `export` if a page times out (configurable with `--timeout`) ([#378](https://github.com/sveltejs/sapper/pull/378))
|
|
||||||
|
|
||||||
## 0.17.1
|
|
||||||
|
|
||||||
* Print which file is causing build errors/warnings ([#371](https://github.com/sveltejs/sapper/pull/371))
|
|
||||||
|
|
||||||
## 0.17.0
|
|
||||||
|
|
||||||
* Use `cheap-watch` instead of `chokidar` ([#364](https://github.com/sveltejs/sapper/issues/364))
|
|
||||||
|
|
||||||
## 0.16.1
|
|
||||||
|
|
||||||
* Fix file watching regression in previous version
|
|
||||||
|
|
||||||
## 0.16.0
|
|
||||||
|
|
||||||
* Slim down installed package ([#363](https://github.com/sveltejs/sapper/pull/363))
|
|
||||||
|
|
||||||
## 0.15.8
|
|
||||||
|
|
||||||
* Only set `preloading: true` on navigation, not prefetch ([#352](https://github.com/sveltejs/sapper/issues/352))
|
|
||||||
* Provide fallback for missing preload errors ([#361](https://github.com/sveltejs/sapper/pull/361))
|
|
||||||
|
|
||||||
## 0.15.7
|
|
||||||
|
|
||||||
* Strip leading slash from redirects ([#291](https://github.com/sveltejs/sapper/issues/291))
|
|
||||||
* Pass `(req, res)` to store getter ([#344](https://github.com/sveltejs/sapper/issues/344))
|
|
||||||
|
|
||||||
## 0.15.6
|
|
||||||
|
|
||||||
* Fix exporting with custom basepath ([#342](https://github.com/sveltejs/sapper/pull/342))
|
|
||||||
|
|
||||||
## 0.15.5
|
## 0.15.5
|
||||||
|
|
||||||
* Faster `export` with more explanatory output ([#335](https://github.com/sveltejs/sapper/pull/335))
|
* Faster `export` with more explanatory output ([#335](https://github.com/sveltejs/sapper/pull/335))
|
||||||
|
|||||||
2
api.js
@@ -1 +1 @@
|
|||||||
module.exports = require('./dist/api.js');
|
module.exports = require('./dist/api.ts.js');
|
||||||
@@ -14,7 +14,7 @@ environment:
|
|||||||
|
|
||||||
install:
|
install:
|
||||||
- ps: Install-Product node $env:nodejs_version
|
- ps: Install-Product node $env:nodejs_version
|
||||||
- npm ci
|
- npm install
|
||||||
|
|
||||||
test_script:
|
test_script:
|
||||||
- node --version && npm --version
|
- node --version && npm --version
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
module.exports = require('../dist/rollup.js');
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports = require('../dist/webpack.js');
|
|
||||||
1
index.js
@@ -1 +0,0 @@
|
|||||||
throw new Error(`As of Sapper 0.22, you should not import 'sapper' directly. See https://sapper.svelte.technology/guide#0-21-to-0-22 for more information`);
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
--require source-map-support/register
|
--require source-map-support/register
|
||||||
--require ts-node/register
|
|
||||||
--recursive
|
--recursive
|
||||||
test/unit/*/*.ts
|
test/unit/*/*.js
|
||||||
test/common/test.js
|
test/common/test.js
|
||||||
5189
package-lock.json
generated
73
package.json
@@ -1,79 +1,74 @@
|
|||||||
{
|
{
|
||||||
"name": "sapper",
|
"name": "sapper",
|
||||||
"version": "0.22.10",
|
"version": "0.15.4",
|
||||||
"description": "Military-grade apps, engineered by Svelte",
|
"description": "Military-grade apps, engineered by Svelte",
|
||||||
|
"main": "dist/middleware.ts.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
"sapper": "./sapper"
|
"sapper": "./sapper"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"*.js",
|
"*.js",
|
||||||
|
"*.ts.js",
|
||||||
|
"runtime",
|
||||||
"webpack",
|
"webpack",
|
||||||
"config",
|
|
||||||
"sapper",
|
"sapper",
|
||||||
"dist/*.js",
|
"components",
|
||||||
"templates/*.js"
|
"dist"
|
||||||
],
|
],
|
||||||
"directories": {
|
"directories": {
|
||||||
"test": "test"
|
"test": "test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"html-minifier": "^3.5.16",
|
"ansi-colors": "^2.0.1",
|
||||||
"shimport": "0.0.11",
|
"cheerio": "^1.0.0-rc.2",
|
||||||
"source-map-support": "^0.5.6",
|
"chokidar": "^2.0.3",
|
||||||
"sourcemap-codec": "^1.4.1",
|
|
||||||
"string-hash": "^1.1.3",
|
|
||||||
"tslib": "^1.9.1"
|
|
||||||
},
|
|
||||||
"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",
|
|
||||||
"agadoo": "^1.0.1",
|
|
||||||
"cheap-watch": "^0.3.0",
|
|
||||||
"compression": "^1.7.1",
|
|
||||||
"cookie": "^0.3.1",
|
"cookie": "^0.3.1",
|
||||||
"devalue": "^1.0.4",
|
"devalue": "^1.0.4",
|
||||||
"eslint": "^4.13.1",
|
"glob": "^7.1.2",
|
||||||
"eslint-plugin-import": "^2.12.0",
|
"html-minifier": "^3.5.16",
|
||||||
"express": "^4.16.3",
|
|
||||||
"kleur": "^2.0.1",
|
|
||||||
"mkdirp": "^0.5.1",
|
"mkdirp": "^0.5.1",
|
||||||
"mocha": "^5.2.0",
|
|
||||||
"nightmare": "^3.0.0",
|
|
||||||
"node-fetch": "^2.1.1",
|
"node-fetch": "^2.1.1",
|
||||||
"npm-run-all": "^4.1.3",
|
|
||||||
"polka": "^0.4.0",
|
|
||||||
"port-authority": "^1.0.5",
|
"port-authority": "^1.0.5",
|
||||||
"pretty-bytes": "^5.0.0",
|
"pretty-bytes": "^5.0.0",
|
||||||
"pretty-ms": "^3.1.0",
|
"pretty-ms": "^3.1.0",
|
||||||
"require-relative": "^0.8.7",
|
"require-relative": "^0.8.7",
|
||||||
"rimraf": "^2.6.2",
|
"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-plugin-string": "^2.0.2",
|
|
||||||
"rollup-plugin-typescript": "^0.8.1",
|
|
||||||
"sade": "^1.4.1",
|
"sade": "^1.4.1",
|
||||||
"sander": "^0.6.0",
|
"sander": "^0.6.0",
|
||||||
|
"source-map-support": "^0.5.6",
|
||||||
|
"tslib": "^1.9.1",
|
||||||
|
"url-parse": "^1.2.0",
|
||||||
|
"webpack-format-messages": "^2.0.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/glob": "^5.0.34",
|
||||||
|
"@types/mkdirp": "^0.5.2",
|
||||||
|
"@types/rimraf": "^2.0.2",
|
||||||
|
"compression": "^1.7.1",
|
||||||
|
"eslint": "^4.13.1",
|
||||||
|
"eslint-plugin-import": "^2.12.0",
|
||||||
|
"express": "^4.16.3",
|
||||||
|
"mocha": "^5.2.0",
|
||||||
|
"nightmare": "^3.0.0",
|
||||||
|
"npm-run-all": "^4.1.3",
|
||||||
|
"polka": "^0.4.0",
|
||||||
|
"rollup": "^0.59.2",
|
||||||
|
"rollup-plugin-commonjs": "^9.1.3",
|
||||||
|
"rollup-plugin-json": "^3.0.0",
|
||||||
|
"rollup-plugin-string": "^2.0.2",
|
||||||
|
"rollup-plugin-typescript": "^0.8.1",
|
||||||
"serve-static": "^1.13.2",
|
"serve-static": "^1.13.2",
|
||||||
"svelte": "^2.6.3",
|
"svelte": "^2.6.3",
|
||||||
"svelte-loader": "^2.9.0",
|
"svelte-loader": "^2.9.0",
|
||||||
"tiny-glob": "^0.2.2",
|
|
||||||
"ts-node": "^7.0.1",
|
|
||||||
"typescript": "^2.8.3",
|
"typescript": "^2.8.3",
|
||||||
"walk-sync": "^0.3.2",
|
"walk-sync": "^0.3.2",
|
||||||
"webpack": "^4.8.3",
|
"webpack": "^4.8.3"
|
||||||
"webpack-format-messages": "^2.0.1"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"cy:open": "cypress open",
|
"cy:open": "cypress open",
|
||||||
"test": "mocha --opts mocha.opts",
|
"test": "mocha --opts mocha.opts",
|
||||||
"pretest": "npm run build",
|
"pretest": "npm run build",
|
||||||
"build": "rm -rf dist && rollup -c",
|
"build": "rm -rf dist && rollup -c",
|
||||||
"prepare": "npm run build",
|
|
||||||
"dev": "rollup -cw",
|
"dev": "rollup -cw",
|
||||||
"prepublishOnly": "npm test",
|
"prepublishOnly": "npm test",
|
||||||
"update_mime_types": "curl http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types | grep -e \"^[^#]\" > src/middleware/mime-types.md"
|
"update_mime_types": "curl http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types | grep -e \"^[^#]\" > src/middleware/mime-types.md"
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import typescript from 'rollup-plugin-typescript';
|
import typescript from 'rollup-plugin-typescript';
|
||||||
import string from 'rollup-plugin-string';
|
import string from 'rollup-plugin-string';
|
||||||
import json from 'rollup-plugin-json';
|
import json from 'rollup-plugin-json';
|
||||||
import resolve from 'rollup-plugin-node-resolve';
|
|
||||||
import commonjs from 'rollup-plugin-commonjs';
|
import commonjs from 'rollup-plugin-commonjs';
|
||||||
import pkg from './package.json';
|
import pkg from './package.json';
|
||||||
import { builtinModules } from 'module';
|
|
||||||
|
|
||||||
const external = [].concat(
|
const external = [].concat(
|
||||||
Object.keys(pkg.dependencies),
|
Object.keys(pkg.dependencies),
|
||||||
@@ -12,38 +10,27 @@ const external = [].concat(
|
|||||||
'sapper/core.js'
|
'sapper/core.js'
|
||||||
);
|
);
|
||||||
|
|
||||||
function template(kind, external) {
|
export default [
|
||||||
return {
|
{
|
||||||
input: `templates/src/${kind}/index.ts`,
|
input: `src/runtime/index.ts`,
|
||||||
output: {
|
output: {
|
||||||
file: `templates/${kind}.js`,
|
file: `runtime.js`,
|
||||||
format: 'es'
|
format: 'es'
|
||||||
},
|
},
|
||||||
external,
|
|
||||||
plugins: [
|
plugins: [
|
||||||
resolve(),
|
|
||||||
commonjs(),
|
|
||||||
string({
|
|
||||||
include: '**/*.md'
|
|
||||||
}),
|
|
||||||
typescript({
|
typescript({
|
||||||
typescript: require('typescript'),
|
typescript: require('typescript'),
|
||||||
target: "ES2017"
|
target: "ES2017"
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
};
|
},
|
||||||
}
|
|
||||||
|
|
||||||
export default [
|
|
||||||
template('client', ['__ROOT__', '__ERROR__']),
|
|
||||||
template('server', builtinModules),
|
|
||||||
|
|
||||||
{
|
{
|
||||||
input: [
|
input: [
|
||||||
`src/api.ts`,
|
`src/api.ts`,
|
||||||
`src/cli.ts`,
|
`src/cli.ts`,
|
||||||
`src/core.ts`,
|
`src/core.ts`,
|
||||||
`src/rollup.ts`,
|
`src/middleware.ts`,
|
||||||
`src/webpack.ts`
|
`src/webpack.ts`
|
||||||
],
|
],
|
||||||
output: {
|
output: {
|
||||||
@@ -53,13 +40,16 @@ export default [
|
|||||||
},
|
},
|
||||||
external,
|
external,
|
||||||
plugins: [
|
plugins: [
|
||||||
|
string({
|
||||||
|
include: '**/*.md'
|
||||||
|
}),
|
||||||
json(),
|
json(),
|
||||||
resolve(),
|
|
||||||
commonjs(),
|
commonjs(),
|
||||||
typescript({
|
typescript({
|
||||||
typescript: require('typescript')
|
typescript: require('typescript')
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
experimentalCodeSplitting: true
|
experimentalCodeSplitting: true,
|
||||||
|
experimentalDynamicImport: true
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
1
runtime/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
This directory exists for legacy reasons and should be deleted before releasing version 1.
|
||||||
2
runtime/app.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
console.error('sapper/runtime/app.js has been deprecated in favour of sapper/runtime.js');
|
||||||
|
export * from '../runtime.js';
|
||||||
2
sapper
@@ -1,2 +1,2 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
require('./dist/cli.js');
|
require('./dist/cli.ts.js');
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
let source;
|
let source;
|
||||||
|
|
||||||
function check() {
|
function check() {
|
||||||
if (typeof module === 'undefined') return;
|
|
||||||
|
|
||||||
if (module.hot.status() === 'idle') {
|
if (module.hot.status() === 'idle') {
|
||||||
module.hot.check(true).then(modules => {
|
module.hot.check(true).then(modules => {
|
||||||
console.log(`[SAPPER] applied HMR update`);
|
console.log(`[SAPPER] applied HMR update`);
|
||||||
|
|||||||
100
src/api/build.ts
@@ -3,22 +3,14 @@ import * as path from 'path';
|
|||||||
import mkdirp from 'mkdirp';
|
import mkdirp from 'mkdirp';
|
||||||
import rimraf from 'rimraf';
|
import rimraf from 'rimraf';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import minify_html from './utils/minify_html';
|
import { minify_html } from './utils/minify_html';
|
||||||
import { create_compilers, create_main_manifests, create_manifest_data, create_serviceworker_manifest } from '../core';
|
import { create_compilers, create_main_manifests, create_routes, create_serviceworker_manifest } from '../core';
|
||||||
import * as events from './interfaces';
|
import * as events from './interfaces';
|
||||||
import { copy_shimport } from './utils/copy_shimport';
|
|
||||||
import { Dirs } from '../interfaces';
|
|
||||||
import read_template from '../core/read_template';
|
|
||||||
|
|
||||||
type Opts = {
|
export function build(opts: {}) {
|
||||||
legacy: boolean;
|
|
||||||
bundler: 'rollup' | 'webpack';
|
|
||||||
};
|
|
||||||
|
|
||||||
export function build(opts: Opts, dirs: Dirs) {
|
|
||||||
const emitter = new EventEmitter();
|
const emitter = new EventEmitter();
|
||||||
|
|
||||||
execute(emitter, opts, dirs).then(
|
execute(emitter, opts).then(
|
||||||
() => {
|
() => {
|
||||||
emitter.emit('done', <events.DoneEvent>{}); // TODO do we need to pass back any info?
|
emitter.emit('done', <events.DoneEvent>{}); // TODO do we need to pass back any info?
|
||||||
},
|
},
|
||||||
@@ -32,14 +24,18 @@ export function build(opts: Opts, dirs: Dirs) {
|
|||||||
return emitter;
|
return emitter;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function execute(emitter: EventEmitter, opts: Opts, dirs: Dirs) {
|
async function execute(emitter: EventEmitter, {
|
||||||
rimraf.sync(path.join(dirs.dest, '**/*'));
|
dest = 'build',
|
||||||
mkdirp.sync(`${dirs.dest}/client`);
|
app = 'app',
|
||||||
copy_shimport(dirs.dest);
|
webpack = 'webpack',
|
||||||
|
routes = 'routes'
|
||||||
|
} = {}) {
|
||||||
|
mkdirp.sync(dest);
|
||||||
|
rimraf.sync(path.join(dest, '**/*'));
|
||||||
|
|
||||||
// minify src/template.html
|
// minify app/template.html
|
||||||
// TODO compile this to a function? could be quicker than str.replace(...).replace(...).replace(...)
|
// TODO compile this to a function? could be quicker than str.replace(...).replace(...).replace(...)
|
||||||
const template = read_template();
|
const template = fs.readFileSync(`${app}/template.html`, 'utf-8');
|
||||||
|
|
||||||
// remove this in a future version
|
// remove this in a future version
|
||||||
if (template.indexOf('%sapper.base%') === -1) {
|
if (template.indexOf('%sapper.base%') === -1) {
|
||||||
@@ -48,64 +44,66 @@ async function execute(emitter: EventEmitter, opts: Opts, dirs: Dirs) {
|
|||||||
throw error;
|
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 route_objects = create_routes();
|
||||||
|
|
||||||
// create src/manifest/client.js and src/manifest/server.js
|
// create app/manifest/client.js and app/manifest/server.js
|
||||||
create_main_manifests({ bundler: opts.bundler, manifest_data });
|
create_main_manifests({ routes: route_objects });
|
||||||
|
|
||||||
const { client, server, serviceworker } = await create_compilers(opts.bundler);
|
const { client, server, serviceworker } = create_compilers({ webpack });
|
||||||
|
|
||||||
const client_result = await client.compile();
|
const client_stats = await compile(client);
|
||||||
emitter.emit('build', <events.BuildEvent>{
|
emitter.emit('build', <events.BuildEvent>{
|
||||||
type: 'client',
|
type: 'client',
|
||||||
// TODO duration/warnings
|
// TODO duration/warnings
|
||||||
result: client_result
|
webpack_stats: client_stats
|
||||||
});
|
});
|
||||||
|
|
||||||
const build_info = client_result.to_json(manifest_data, dirs);
|
const client_info = client_stats.toJson();
|
||||||
|
fs.writeFileSync(path.join(dest, 'client_assets.json'), JSON.stringify(client_info.assetsByChunkName));
|
||||||
|
|
||||||
if (opts.legacy) {
|
const server_stats = await compile(server);
|
||||||
process.env.SAPPER_LEGACY_BUILD = 'true';
|
|
||||||
const { client } = await create_compilers(opts.bundler);
|
|
||||||
|
|
||||||
const client_result = await client.compile();
|
|
||||||
|
|
||||||
emitter.emit('build', <events.BuildEvent>{
|
|
||||||
type: 'client (legacy)',
|
|
||||||
// TODO duration/warnings
|
|
||||||
result: client_result
|
|
||||||
});
|
|
||||||
|
|
||||||
client_result.to_json(manifest_data, dirs);
|
|
||||||
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));
|
|
||||||
|
|
||||||
const server_stats = await server.compile();
|
|
||||||
emitter.emit('build', <events.BuildEvent>{
|
emitter.emit('build', <events.BuildEvent>{
|
||||||
type: 'server',
|
type: 'server',
|
||||||
// TODO duration/warnings
|
// TODO duration/warnings
|
||||||
result: server_stats
|
webpack_stats: server_stats
|
||||||
});
|
});
|
||||||
|
|
||||||
let serviceworker_stats;
|
let serviceworker_stats;
|
||||||
|
|
||||||
if (serviceworker) {
|
if (serviceworker) {
|
||||||
create_serviceworker_manifest({
|
create_serviceworker_manifest({
|
||||||
manifest_data,
|
routes: route_objects,
|
||||||
client_files: client_result.chunks.map(chunk => `client/${chunk.file}`)
|
client_files: client_stats.toJson().assets.map((chunk: { name: string }) => `client/${chunk.name}`)
|
||||||
});
|
});
|
||||||
|
|
||||||
serviceworker_stats = await serviceworker.compile();
|
serviceworker_stats = await compile(serviceworker);
|
||||||
|
|
||||||
emitter.emit('build', <events.BuildEvent>{
|
emitter.emit('build', <events.BuildEvent>{
|
||||||
type: 'serviceworker',
|
type: 'serviceworker',
|
||||||
// TODO duration/warnings
|
// TODO duration/warnings
|
||||||
result: serviceworker_stats
|
webpack_stats: serviceworker_stats
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function compile(compiler: any) {
|
||||||
|
return new Promise((fulfil, reject) => {
|
||||||
|
compiler.run((err: Error, stats: any) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stats.hasErrors()) {
|
||||||
|
console.error(stats.toString({ colors: true }));
|
||||||
|
reject(new Error(`Encountered errors while building app`));
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
fulfil(stats);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
267
src/api/dev.ts
@@ -5,44 +5,34 @@ import * as child_process from 'child_process';
|
|||||||
import * as ports from 'port-authority';
|
import * as ports from 'port-authority';
|
||||||
import mkdirp from 'mkdirp';
|
import mkdirp from 'mkdirp';
|
||||||
import rimraf from 'rimraf';
|
import rimraf from 'rimraf';
|
||||||
|
import format_messages from 'webpack-format-messages';
|
||||||
import { locations } from '../config';
|
import { locations } from '../config';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { create_manifest_data, create_main_manifests, create_compilers, create_serviceworker_manifest } from '../core';
|
import { create_routes, create_main_manifests, create_compilers, create_serviceworker_manifest } from '../core';
|
||||||
import { Compiler, Compilers } from '../core/create_compilers';
|
|
||||||
import { CompileResult, CompileError } from '../core/create_compilers/interfaces';
|
|
||||||
import Deferred from './utils/Deferred';
|
import Deferred from './utils/Deferred';
|
||||||
import * as events from './interfaces';
|
import * as events from './interfaces';
|
||||||
import validate_bundler from '../cli/utils/validate_bundler';
|
|
||||||
import { copy_shimport } from './utils/copy_shimport';
|
|
||||||
import { ManifestData } from '../interfaces';
|
|
||||||
import read_template from '../core/read_template';
|
|
||||||
|
|
||||||
export function dev(opts) {
|
export function dev(opts) {
|
||||||
return new Watcher(opts);
|
return new Watcher(opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
class Watcher extends EventEmitter {
|
class Watcher extends EventEmitter {
|
||||||
bundler: string;
|
|
||||||
dirs: {
|
dirs: {
|
||||||
src: string;
|
app: string;
|
||||||
dest: string;
|
dest: string;
|
||||||
routes: string;
|
routes: string;
|
||||||
rollup: string;
|
|
||||||
webpack: string;
|
webpack: string;
|
||||||
}
|
}
|
||||||
port: number;
|
port: number;
|
||||||
closed: boolean;
|
closed: boolean;
|
||||||
|
|
||||||
dev_port: number;
|
|
||||||
live: boolean;
|
|
||||||
hot: boolean;
|
|
||||||
|
|
||||||
devtools_port: number;
|
|
||||||
|
|
||||||
dev_server: DevServer;
|
dev_server: DevServer;
|
||||||
proc: child_process.ChildProcess;
|
proc: child_process.ChildProcess;
|
||||||
filewatchers: Array<{ close: () => void }>;
|
filewatchers: Array<{ close: () => void }>;
|
||||||
deferred: Deferred;
|
deferreds: {
|
||||||
|
client: Deferred;
|
||||||
|
server: Deferred;
|
||||||
|
};
|
||||||
|
|
||||||
crashed: boolean;
|
crashed: boolean;
|
||||||
restarting: boolean;
|
restarting: boolean;
|
||||||
@@ -54,43 +44,24 @@ class Watcher extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
src = locations.src(),
|
app = locations.app(),
|
||||||
dest = locations.dest(),
|
dest = locations.dest(),
|
||||||
routes = locations.routes(),
|
routes = locations.routes(),
|
||||||
'dev-port': dev_port,
|
|
||||||
live,
|
|
||||||
hot,
|
|
||||||
'devtools-port': devtools_port,
|
|
||||||
bundler,
|
|
||||||
webpack = 'webpack',
|
webpack = 'webpack',
|
||||||
rollup = 'rollup',
|
|
||||||
port = +process.env.PORT
|
port = +process.env.PORT
|
||||||
}: {
|
}: {
|
||||||
src: string,
|
app: string,
|
||||||
dest: string,
|
dest: string,
|
||||||
routes: string,
|
routes: string,
|
||||||
'dev-port': number,
|
|
||||||
live: boolean,
|
|
||||||
hot: boolean,
|
|
||||||
'devtools-port': number,
|
|
||||||
bundler?: string,
|
|
||||||
webpack: string,
|
webpack: string,
|
||||||
rollup: string,
|
|
||||||
port: number
|
port: number
|
||||||
}) {
|
}) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.bundler = validate_bundler(bundler);
|
this.dirs = { app, dest, routes, webpack };
|
||||||
this.dirs = { src, dest, routes, webpack, rollup };
|
|
||||||
this.port = port;
|
this.port = port;
|
||||||
this.closed = false;
|
this.closed = false;
|
||||||
|
|
||||||
this.dev_port = dev_port;
|
|
||||||
this.live = live;
|
|
||||||
this.hot = hot;
|
|
||||||
|
|
||||||
this.devtools_port = devtools_port;
|
|
||||||
|
|
||||||
this.filewatchers = [];
|
this.filewatchers = [];
|
||||||
|
|
||||||
this.current_build = {
|
this.current_build = {
|
||||||
@@ -101,7 +72,7 @@ class Watcher extends EventEmitter {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// remove this in a future version
|
// remove this in a future version
|
||||||
const template = read_template();
|
const template = fs.readFileSync(path.join(app, 'template.html'), 'utf-8');
|
||||||
if (template.indexOf('%sapper.base%') === -1) {
|
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>`);
|
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`;
|
error.code = `missing-sapper-base`;
|
||||||
@@ -131,19 +102,13 @@ class Watcher extends EventEmitter {
|
|||||||
|
|
||||||
const { dest } = this.dirs;
|
const { dest } = this.dirs;
|
||||||
rimraf.sync(dest);
|
rimraf.sync(dest);
|
||||||
mkdirp.sync(`${dest}/client`);
|
mkdirp.sync(dest);
|
||||||
if (this.bundler === 'rollup') copy_shimport(dest);
|
|
||||||
|
|
||||||
if (!this.dev_port) this.dev_port = await ports.find(10000);
|
const dev_port = await ports.find(10000);
|
||||||
|
|
||||||
// Chrome looks for debugging targets on ports 9222 and 9229 by default
|
|
||||||
if (!this.devtools_port) this.devtools_port = await ports.find(9222);
|
|
||||||
|
|
||||||
let manifest_data: ManifestData;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
manifest_data = create_manifest_data();
|
const routes = create_routes();
|
||||||
create_main_manifests({ bundler: this.bundler, manifest_data, dev_port: this.dev_port });
|
create_main_manifests({ routes, dev_port });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.emit('fatal', <events.FatalEvent>{
|
this.emit('fatal', <events.FatalEvent>{
|
||||||
message: err.message
|
message: err.message
|
||||||
@@ -151,42 +116,37 @@ class Watcher extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dev_server = new DevServer(this.dev_port);
|
this.dev_server = new DevServer(dev_port);
|
||||||
|
|
||||||
this.filewatchers.push(
|
this.filewatchers.push(
|
||||||
watch_dir(
|
watch_files(locations.routes(), ['add', 'unlink'], () => {
|
||||||
locations.routes(),
|
const routes = create_routes();
|
||||||
({ path: file, stats }) => {
|
create_main_manifests({ routes, dev_port });
|
||||||
if (stats.isDirectory()) {
|
|
||||||
return path.basename(file)[0] !== '_';
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
try {
|
|
||||||
const new_manifest_data = create_manifest_data();
|
|
||||||
create_main_manifests({ bundler: this.bundler, manifest_data, dev_port: this.dev_port });
|
|
||||||
|
|
||||||
manifest_data = new_manifest_data;
|
try {
|
||||||
} catch (err) {
|
const routes = create_routes();
|
||||||
this.emit('error', <events.ErrorEvent>{
|
create_main_manifests({ routes, dev_port });
|
||||||
message: err.message
|
} catch (err) {
|
||||||
});
|
this.emit('error', <events.ErrorEvent>{
|
||||||
}
|
message: err.message
|
||||||
|
});
|
||||||
}
|
}
|
||||||
),
|
}),
|
||||||
|
|
||||||
fs.watch(`${locations.src()}/template.html`, () => {
|
watch_files(`${locations.app()}/template.html`, ['change'], () => {
|
||||||
this.dev_server.send({
|
this.dev_server.send({
|
||||||
action: 'reload'
|
action: 'reload'
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
let deferred = new Deferred();
|
this.deferreds = {
|
||||||
|
server: new Deferred(),
|
||||||
|
client: new Deferred()
|
||||||
|
};
|
||||||
|
|
||||||
// TODO watch the configs themselves?
|
// TODO watch the configs themselves?
|
||||||
const compilers: Compilers = await create_compilers(this.bundler, this.dirs);
|
const compilers = create_compilers({ webpack: this.dirs.webpack });
|
||||||
|
|
||||||
let log = '';
|
let log = '';
|
||||||
|
|
||||||
@@ -205,10 +165,11 @@ class Watcher extends EventEmitter {
|
|||||||
|
|
||||||
invalid: filename => {
|
invalid: filename => {
|
||||||
this.restart(filename, 'server');
|
this.restart(filename, 'server');
|
||||||
|
this.deferreds.server = new Deferred();
|
||||||
},
|
},
|
||||||
|
|
||||||
handle_result: (result: CompileResult) => {
|
result: info => {
|
||||||
deferred.promise.then(() => {
|
this.deferreds.client.promise.then(() => {
|
||||||
const restart = () => {
|
const restart = () => {
|
||||||
log = '';
|
log = '';
|
||||||
this.crashed = false;
|
this.crashed = false;
|
||||||
@@ -220,15 +181,11 @@ class Watcher extends EventEmitter {
|
|||||||
process: this.proc
|
process: this.proc
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.hot && this.bundler === 'webpack') {
|
this.deferreds.server.fulfil();
|
||||||
this.dev_server.send({
|
|
||||||
status: 'completed'
|
this.dev_server.send({
|
||||||
});
|
status: 'completed'
|
||||||
} else {
|
});
|
||||||
this.dev_server.send({
|
|
||||||
action: 'reload'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}))
|
}))
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
if (this.crashed) return;
|
if (this.crashed) return;
|
||||||
@@ -248,21 +205,12 @@ class Watcher extends EventEmitter {
|
|||||||
restart();
|
restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
// we need to give the child process its own DevTools port,
|
this.proc = child_process.fork(`${dest}/server.js`, [], {
|
||||||
// otherwise Node will try to use the parent's (and fail)
|
|
||||||
const debugArgRegex = /--inspect(?:-brk|-port)?|--debug-port/;
|
|
||||||
const execArgv = process.execArgv.slice();
|
|
||||||
if (execArgv.some((arg: string) => !!arg.match(debugArgRegex))) {
|
|
||||||
execArgv.push(`--inspect-port=${this.devtools_port}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.proc = child_process.fork(`${dest}/server/server.js`, [], {
|
|
||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
env: Object.assign({
|
env: Object.assign({
|
||||||
PORT: this.port
|
PORT: this.port
|
||||||
}, process.env),
|
}, process.env),
|
||||||
stdio: ['ipc'],
|
stdio: ['ipc']
|
||||||
execArgv
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.proc.stdout.on('data', chunk => {
|
this.proc.stdout.on('data', chunk => {
|
||||||
@@ -276,11 +224,8 @@ class Watcher extends EventEmitter {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.proc.on('message', message => {
|
this.proc.on('message', message => {
|
||||||
if (message.__sapper__ && message.event === 'basepath') {
|
if (!message.__sapper__) return;
|
||||||
this.emit('basepath', {
|
this.emit(message.event, message);
|
||||||
basepath: message.basepath
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.proc.on('exit', emitFatal);
|
this.proc.on('exit', emitFatal);
|
||||||
@@ -288,35 +233,31 @@ class Watcher extends EventEmitter {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let first = true;
|
||||||
|
|
||||||
this.watch(compilers.client, {
|
this.watch(compilers.client, {
|
||||||
name: 'client',
|
name: 'client',
|
||||||
|
|
||||||
invalid: filename => {
|
invalid: filename => {
|
||||||
this.restart(filename, 'client');
|
this.restart(filename, 'client');
|
||||||
deferred = new Deferred();
|
this.deferreds.client = new Deferred();
|
||||||
|
|
||||||
// TODO we should delete old assets. due to a webpack bug
|
// TODO we should delete old assets. due to a webpack bug
|
||||||
// i don't even begin to comprehend, this is apparently
|
// i don't even begin to comprehend, this is apparently
|
||||||
// quite difficult
|
// quite difficult
|
||||||
},
|
},
|
||||||
|
|
||||||
handle_result: (result: CompileResult) => {
|
result: info => {
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(path.join(dest, 'client_assets.json'), JSON.stringify(info.assetsByChunkName, null, ' '));
|
||||||
path.join(dest, 'build.json'),
|
this.deferreds.client.fulfil();
|
||||||
|
|
||||||
// TODO should be more explicit that to_json has effects
|
const client_files = info.assets.map((chunk: { name: string }) => `client/${chunk.name}`);
|
||||||
JSON.stringify(result.to_json(manifest_data, this.dirs), null, ' ')
|
|
||||||
);
|
|
||||||
|
|
||||||
const client_files = result.chunks.map(chunk => `client/${chunk.file}`);
|
|
||||||
|
|
||||||
create_serviceworker_manifest({
|
create_serviceworker_manifest({
|
||||||
manifest_data,
|
routes: create_routes(),
|
||||||
client_files
|
client_files
|
||||||
});
|
});
|
||||||
|
|
||||||
deferred.fulfil();
|
|
||||||
|
|
||||||
// we need to wait a beat before watching the service
|
// we need to wait a beat before watching the service
|
||||||
// worker, because of some webpack nonsense
|
// worker, because of some webpack nonsense
|
||||||
setTimeout(watch_serviceworker, 100);
|
setTimeout(watch_serviceworker, 100);
|
||||||
@@ -328,7 +269,11 @@ class Watcher extends EventEmitter {
|
|||||||
watch_serviceworker = noop;
|
watch_serviceworker = noop;
|
||||||
|
|
||||||
this.watch(compilers.serviceworker, {
|
this.watch(compilers.serviceworker, {
|
||||||
name: 'service worker'
|
name: 'service worker',
|
||||||
|
|
||||||
|
result: info => {
|
||||||
|
fs.writeFileSync(path.join(dest, 'serviceworker_info.json'), JSON.stringify(info, null, ' '));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
: noop;
|
: noop;
|
||||||
@@ -375,34 +320,82 @@ class Watcher extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(compiler: Compiler, { name, invalid = noop, handle_result = noop }: {
|
watch(compiler: any, { name, invalid = noop, result }: {
|
||||||
name: string,
|
name: string,
|
||||||
invalid?: (filename: string) => void;
|
invalid?: (filename: string) => void;
|
||||||
handle_result?: (result: CompileResult) => void;
|
result: (stats: any) => void;
|
||||||
}) {
|
}) {
|
||||||
compiler.oninvalid(invalid);
|
compiler.hooks.invalid.tap('sapper', (filename: string) => {
|
||||||
|
invalid(filename);
|
||||||
|
});
|
||||||
|
|
||||||
compiler.watch((err?: Error, result?: CompileResult) => {
|
compiler.watch({}, (err: Error, stats: any) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.emit('error', <events.ErrorEvent>{
|
this.emit('error', <events.ErrorEvent>{
|
||||||
type: name,
|
type: name,
|
||||||
message: err.message
|
message: err.message
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
const messages = format_messages(stats);
|
||||||
|
const info = stats.toJson();
|
||||||
|
|
||||||
this.emit('build', {
|
this.emit('build', {
|
||||||
type: name,
|
type: name,
|
||||||
|
|
||||||
duration: result.duration,
|
duration: info.time,
|
||||||
errors: result.errors,
|
|
||||||
warnings: result.warnings
|
errors: messages.errors.map((message: string) => {
|
||||||
|
const duplicate = this.current_build.unique_errors.has(message);
|
||||||
|
this.current_build.unique_errors.add(message);
|
||||||
|
|
||||||
|
return mungeWebpackError(message, duplicate);
|
||||||
|
}),
|
||||||
|
|
||||||
|
warnings: messages.warnings.map((message: string) => {
|
||||||
|
const duplicate = this.current_build.unique_warnings.has(message);
|
||||||
|
this.current_build.unique_warnings.add(message);
|
||||||
|
|
||||||
|
return mungeWebpackError(message, duplicate);
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
handle_result(result);
|
result(info);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const locPattern = /\((\d+):(\d+)\)$/;
|
||||||
|
|
||||||
|
function mungeWebpackError(message: string, duplicate: boolean) {
|
||||||
|
// TODO this is all a bit rube goldberg...
|
||||||
|
const lines = message.split('\n');
|
||||||
|
|
||||||
|
const file = lines.shift()
|
||||||
|
.replace('[7m', '') // careful — there is a special character at the beginning of this string
|
||||||
|
.replace('[27m', '')
|
||||||
|
.replace('./', '');
|
||||||
|
|
||||||
|
let line = null;
|
||||||
|
let column = null;
|
||||||
|
|
||||||
|
const match = locPattern.exec(lines[0]);
|
||||||
|
if (match) {
|
||||||
|
lines[0] = lines[0].replace(locPattern, '');
|
||||||
|
line = +match[1];
|
||||||
|
column = +match[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
file,
|
||||||
|
line,
|
||||||
|
column,
|
||||||
|
message: lines.join('\n'),
|
||||||
|
originalMessage: message,
|
||||||
|
duplicate
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const INTERVAL = 10000;
|
const INTERVAL = 10000;
|
||||||
|
|
||||||
class DevServer {
|
class DevServer {
|
||||||
@@ -457,32 +450,20 @@ class DevServer {
|
|||||||
|
|
||||||
function noop() {}
|
function noop() {}
|
||||||
|
|
||||||
function watch_dir(
|
function watch_files(pattern: string, events: string[], callback: () => void) {
|
||||||
dir: string,
|
const chokidar = require('chokidar');
|
||||||
filter: ({ path, stats }: { path: string, stats: fs.Stats }) => boolean,
|
|
||||||
callback: () => void
|
|
||||||
) {
|
|
||||||
let watch;
|
|
||||||
let closed = false;
|
|
||||||
|
|
||||||
import('cheap-watch').then(CheapWatch => {
|
const watcher = chokidar.watch(pattern, {
|
||||||
if (closed) return;
|
persistent: true,
|
||||||
|
ignoreInitial: true,
|
||||||
|
disableGlobbing: true
|
||||||
|
});
|
||||||
|
|
||||||
watch = new CheapWatch({ dir, filter, debounce: 50 });
|
events.forEach(event => {
|
||||||
|
watcher.on(event, callback);
|
||||||
watch.on('+', ({ isNew }) => {
|
|
||||||
if (isNew) callback();
|
|
||||||
});
|
|
||||||
|
|
||||||
watch.on('-', callback);
|
|
||||||
|
|
||||||
watch.init();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
close: () => {
|
close: () => watcher.close()
|
||||||
if (watch) watch.close();
|
|
||||||
closed = true;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,16 @@
|
|||||||
import * as child_process from 'child_process';
|
import * as child_process from 'child_process';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as sander from 'sander';
|
import * as sander from 'sander';
|
||||||
import * as url from 'url';
|
import cheerio from 'cheerio';
|
||||||
|
import URL from 'url-parse';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
import * as ports from 'port-authority';
|
import * as ports from 'port-authority';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import clean_html from './utils/clean_html';
|
import { minify_html } from './utils/minify_html';
|
||||||
import minify_html from './utils/minify_html';
|
|
||||||
import Deferred from './utils/Deferred';
|
import Deferred from './utils/Deferred';
|
||||||
import * as events from './interfaces';
|
import * as events from './interfaces';
|
||||||
|
|
||||||
type Opts = {
|
export function exporter(opts: {}) {
|
||||||
build: string,
|
|
||||||
dest: string,
|
|
||||||
static: string,
|
|
||||||
basepath?: string,
|
|
||||||
timeout: number | false
|
|
||||||
};
|
|
||||||
|
|
||||||
export function exporter(opts: Opts) {
|
|
||||||
const emitter = new EventEmitter();
|
const emitter = new EventEmitter();
|
||||||
|
|
||||||
execute(emitter, opts).then(
|
execute(emitter, opts).then(
|
||||||
@@ -35,63 +27,68 @@ export function exporter(opts: Opts) {
|
|||||||
return emitter;
|
return emitter;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolve(from: string, to: string) {
|
async function execute(emitter: EventEmitter, {
|
||||||
return url.parse(url.resolve(from, to));
|
build = 'build',
|
||||||
}
|
dest = 'export',
|
||||||
|
basepath = ''
|
||||||
type URL = url.UrlWithStringQuery;
|
} = {}) {
|
||||||
|
const export_dir = path.join(dest, basepath);
|
||||||
async function execute(emitter: EventEmitter, opts: Opts) {
|
|
||||||
const export_dir = path.join(opts.dest, opts.basepath);
|
|
||||||
|
|
||||||
// Prep output directory
|
// Prep output directory
|
||||||
sander.rimrafSync(export_dir);
|
sander.rimrafSync(export_dir);
|
||||||
|
|
||||||
sander.copydirSync(opts.static).to(export_dir);
|
sander.copydirSync('assets').to(export_dir);
|
||||||
sander.copydirSync(opts.build, 'client').to(export_dir, 'client');
|
sander.copydirSync(build, 'client').to(export_dir, 'client');
|
||||||
|
|
||||||
if (sander.existsSync(opts.build, 'service-worker.js')) {
|
if (sander.existsSync(build, 'service-worker.js')) {
|
||||||
sander.copyFileSync(opts.build, 'service-worker.js').to(export_dir, 'service-worker.js');
|
sander.copyFileSync(build, 'service-worker.js').to(export_dir, 'service-worker.js');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sander.existsSync(opts.build, 'service-worker.js.map')) {
|
if (sander.existsSync(build, 'service-worker.js.map')) {
|
||||||
sander.copyFileSync(opts.build, 'service-worker.js.map').to(export_dir, 'service-worker.js.map');
|
sander.copyFileSync(build, 'service-worker.js.map').to(export_dir, 'service-worker.js.map');
|
||||||
}
|
}
|
||||||
|
|
||||||
const port = await ports.find(3000);
|
const port = await ports.find(3000);
|
||||||
|
|
||||||
const protocol = 'http:';
|
const origin = `http://localhost:${port}`;
|
||||||
const host = `localhost:${port}`;
|
|
||||||
const origin = `${protocol}//${host}`;
|
|
||||||
|
|
||||||
const root = resolve(origin, opts.basepath || '');
|
|
||||||
if (!root.href.endsWith('/')) root.href += '/';
|
|
||||||
|
|
||||||
emitter.emit('info', {
|
emitter.emit('info', {
|
||||||
message: `Crawling ${root.href}`
|
message: `Crawling ${origin}`
|
||||||
});
|
});
|
||||||
|
|
||||||
const proc = child_process.fork(path.resolve(`${opts.build}/server/server.js`), [], {
|
const proc = child_process.fork(path.resolve(`${build}/server.js`), [], {
|
||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
env: Object.assign({
|
env: Object.assign({
|
||||||
PORT: port,
|
PORT: port,
|
||||||
NODE_ENV: 'production',
|
NODE_ENV: 'production',
|
||||||
SAPPER_DEST: opts.build,
|
SAPPER_DEST: build,
|
||||||
SAPPER_EXPORT: 'true'
|
SAPPER_EXPORT: 'true'
|
||||||
}, process.env)
|
}, process.env)
|
||||||
});
|
});
|
||||||
|
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
const saved = new Set();
|
const saved = new Set();
|
||||||
|
const deferreds = new Map();
|
||||||
|
|
||||||
function save(path: string, status: number, type: string, body: string) {
|
function get_deferred(pathname: string) {
|
||||||
const { pathname } = resolve(origin, path);
|
if (!deferreds.has(pathname)) {
|
||||||
let file = decodeURIComponent(pathname.slice(1));
|
deferreds.set(pathname, new Deferred()) ;
|
||||||
|
}
|
||||||
|
|
||||||
|
return deferreds.get(pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
proc.on('message', message => {
|
||||||
|
if (!message.__sapper__ || message.event !== 'file') return;
|
||||||
|
|
||||||
|
const pathname = new URL(message.url, origin).pathname;
|
||||||
|
let file = pathname.slice(1);
|
||||||
|
let { body } = message;
|
||||||
|
|
||||||
if (saved.has(file)) return;
|
if (saved.has(file)) return;
|
||||||
saved.add(file);
|
saved.add(file);
|
||||||
|
|
||||||
const is_html = type === 'text/html';
|
const is_html = message.type === 'text/html';
|
||||||
|
|
||||||
if (is_html) {
|
if (is_html) {
|
||||||
file = file === '' ? 'index.html' : `${file}/index.html`;
|
file = file === '' ? 'index.html' : `${file}/index.html`;
|
||||||
@@ -101,94 +98,46 @@ async function execute(emitter: EventEmitter, opts: Opts) {
|
|||||||
emitter.emit('file', <events.FileEvent>{
|
emitter.emit('file', <events.FileEvent>{
|
||||||
file,
|
file,
|
||||||
size: body.length,
|
size: body.length,
|
||||||
status
|
status: message.status
|
||||||
});
|
});
|
||||||
|
|
||||||
sander.writeFileSync(export_dir, file, body);
|
sander.writeFileSync(export_dir, file, body);
|
||||||
}
|
|
||||||
|
|
||||||
proc.on('message', message => {
|
get_deferred(pathname).fulfil();
|
||||||
if (!message.__sapper__ || message.event !== 'file') return;
|
|
||||||
save(message.url, message.status, message.type, message.body);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
async function handle(url: URL) {
|
async function handle(url: URL) {
|
||||||
const pathname = (url.pathname.replace(root.pathname, '') || '/');
|
const pathname = url.pathname || '/';
|
||||||
|
|
||||||
if (seen.has(pathname)) return;
|
if (seen.has(pathname)) return;
|
||||||
seen.add(pathname);
|
seen.add(pathname);
|
||||||
|
|
||||||
const timeout_deferred = new Deferred();
|
const deferred = get_deferred(pathname);
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
timeout_deferred.reject(new Error(`Timed out waiting for ${url.href}`));
|
|
||||||
}, opts.timeout);
|
|
||||||
|
|
||||||
const r = await Promise.race([
|
|
||||||
fetch(url.href, {
|
|
||||||
redirect: 'manual'
|
|
||||||
}),
|
|
||||||
timeout_deferred.promise
|
|
||||||
]);
|
|
||||||
|
|
||||||
clearTimeout(timeout); // prevent it hanging at the end
|
|
||||||
|
|
||||||
let type = r.headers.get('Content-Type');
|
|
||||||
let body = await r.text();
|
|
||||||
|
|
||||||
|
const r = await fetch(url.href);
|
||||||
const range = ~~(r.status / 100);
|
const range = ~~(r.status / 100);
|
||||||
|
|
||||||
if (range === 2) {
|
if (range === 2) {
|
||||||
if (type === 'text/html') {
|
if (r.headers.get('Content-Type') === 'text/html') {
|
||||||
|
const body = await r.text();
|
||||||
|
const $ = cheerio.load(body);
|
||||||
const urls: URL[] = [];
|
const urls: URL[] = [];
|
||||||
|
|
||||||
const cleaned = clean_html(body);
|
const base = new URL($('base').attr('href') || '/', url.href);
|
||||||
|
|
||||||
const base_match = /<base ([\s\S]+?)>/m.exec(cleaned);
|
$('a[href]').each((i: number, $a) => {
|
||||||
const base_href = base_match && get_href(base_match[1]);
|
const url = new URL($a.attribs.href, base.href);
|
||||||
const base = resolve(url.href, base_href);
|
if (url.origin === origin) urls.push(url);
|
||||||
|
});
|
||||||
let match;
|
|
||||||
let pattern = /<a ([\s\S]+?)>/gm;
|
|
||||||
|
|
||||||
while (match = pattern.exec(cleaned)) {
|
|
||||||
const attrs = match[1];
|
|
||||||
const href = get_href(attrs);
|
|
||||||
|
|
||||||
if (href) {
|
|
||||||
const url = resolve(base.href, href);
|
|
||||||
|
|
||||||
if (url.protocol === protocol && url.host === host) {
|
|
||||||
urls.push(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(urls.map(handle));
|
await Promise.all(urls.map(handle));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (range === 3) {
|
await deferred.promise;
|
||||||
const location = r.headers.get('Location');
|
|
||||||
|
|
||||||
type = 'text/html';
|
|
||||||
body = `<script>window.location.href = "${location.replace(origin, '')}"</script>`;
|
|
||||||
|
|
||||||
await handle(resolve(root.href, location));
|
|
||||||
}
|
|
||||||
|
|
||||||
save(pathname, r.status, type, body);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ports.wait(port)
|
return ports.wait(port)
|
||||||
.then(() => handle(root))
|
.then(() => handle(new URL(`/${basepath}`, origin))) // TODO all static routes
|
||||||
.then(() => proc.kill())
|
.then(() => proc.kill());
|
||||||
.catch(err => {
|
|
||||||
proc.kill();
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_href(attrs: string) {
|
|
||||||
const match = /href\s*=\s*(?:"(.+?)"|'(.+?)'|([^\s>]+))/.exec(attrs);
|
|
||||||
return match[1] || match[2] || match[3];
|
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import * as glob from 'glob';
|
||||||
import { locations } from '../config';
|
import { locations } from '../config';
|
||||||
import { create_manifest_data } from '../core';
|
import { create_routes } from '../core';
|
||||||
|
|
||||||
export function find_page(pathname: string, cwd = locations.routes()) {
|
export function find_page(pathname: string, cwd = locations.routes()) {
|
||||||
const { pages } = create_manifest_data(cwd);
|
const { pages } = create_routes(cwd);
|
||||||
|
|
||||||
for (let i = 0; i < pages.length; i += 1) {
|
for (let i = 0; i < pages.length; i += 1) {
|
||||||
const page = pages[i];
|
const page = pages[i];
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import * as child_process from 'child_process';
|
import * as child_process from 'child_process';
|
||||||
import { CompileResult } from '../core/create_compilers/interfaces';
|
|
||||||
|
|
||||||
export type ReadyEvent = {
|
export type ReadyEvent = {
|
||||||
port: number;
|
port: number;
|
||||||
@@ -27,10 +26,10 @@ export type InvalidEvent = {
|
|||||||
|
|
||||||
export type BuildEvent = {
|
export type BuildEvent = {
|
||||||
type: string;
|
type: string;
|
||||||
errors: Array<{ file: string, message: string, duplicate: boolean }>;
|
errors: Array<{ message: string, duplicate: boolean }>;
|
||||||
warnings: Array<{ file: string, message: string, duplicate: boolean }>;
|
warnings: Array<{ message: string, duplicate: boolean }>;
|
||||||
duration: number;
|
duration: number;
|
||||||
result: CompileResult;
|
webpack_stats: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FileEvent = {
|
export type FileEvent = {
|
||||||
@@ -42,4 +41,4 @@ export type FailureEvent = {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DoneEvent = {};
|
export type DoneEvent = {}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export default function clean_html(html: string) {
|
|
||||||
return html
|
|
||||||
.replace(/<!\[CDATA\[[\s\S]*?\]\]>/gm, '')
|
|
||||||
.replace(/(<script[\s\S]*?>)[\s\S]*?<\/script>/gm, '$1</' + 'script>')
|
|
||||||
.replace(/(<style[\s\S]*?>)[\s\S]*?<\/style>/gm, '$1</' + 'style>')
|
|
||||||
.replace(/<!--[\s\S]*?-->/gm, '');
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import * as fs from 'fs';
|
|
||||||
|
|
||||||
export function copy_shimport(dest: string) {
|
|
||||||
const shimport_version = require('shimport/package.json').version;
|
|
||||||
fs.writeFileSync(
|
|
||||||
`${dest}/client/shimport@${shimport_version}.js`,
|
|
||||||
fs.readFileSync(require.resolve('shimport/index.js'))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { minify } from 'html-minifier';
|
import { minify } from 'html-minifier';
|
||||||
|
|
||||||
export default function minify_html(html: string) {
|
export function minify_html(html: string) {
|
||||||
return minify(html, {
|
return minify(html, {
|
||||||
collapseBooleanAttributes: true,
|
collapseBooleanAttributes: true,
|
||||||
collapseWhitespace: true,
|
collapseWhitespace: true,
|
||||||
|
|||||||
47
src/cli.ts
@@ -1,7 +1,7 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import sade from 'sade';
|
import sade from 'sade';
|
||||||
import colors from 'kleur';
|
import * as colors from 'ansi-colors';
|
||||||
import prettyMs from 'pretty-ms';
|
import prettyMs from 'pretty-ms';
|
||||||
import * as pkg from '../package.json';
|
import * as pkg from '../package.json';
|
||||||
|
|
||||||
@@ -11,18 +11,7 @@ prog.command('dev')
|
|||||||
.describe('Start a development server')
|
.describe('Start a development server')
|
||||||
.option('-p, --port', 'Specify a port')
|
.option('-p, --port', 'Specify a port')
|
||||||
.option('-o, --open', 'Open a browser window')
|
.option('-o, --open', 'Open a browser window')
|
||||||
.option('--dev-port', 'Specify a port for development server')
|
.action(async (opts: { port: number, open: boolean }) => {
|
||||||
.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)')
|
|
||||||
.action(async (opts: {
|
|
||||||
port: number,
|
|
||||||
open: boolean,
|
|
||||||
'dev-port': number,
|
|
||||||
live: boolean,
|
|
||||||
hot: boolean,
|
|
||||||
bundler?: string
|
|
||||||
}) => {
|
|
||||||
const { dev } = await import('./cli/dev');
|
const { dev } = await import('./cli/dev');
|
||||||
dev(opts);
|
dev(opts);
|
||||||
});
|
});
|
||||||
@@ -30,14 +19,8 @@ prog.command('dev')
|
|||||||
prog.command('build [dest]')
|
prog.command('build [dest]')
|
||||||
.describe('Create a production-ready version of your app')
|
.describe('Create a production-ready version of your app')
|
||||||
.option('-p, --port', 'Default of process.env.PORT', '3000')
|
.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')
|
|
||||||
.example(`build custom-dir -p 4567`)
|
.example(`build custom-dir -p 4567`)
|
||||||
.action(async (dest = '__sapper__/build', opts: {
|
.action(async (dest = 'build', opts: { port: string }) => {
|
||||||
port: string,
|
|
||||||
legacy: boolean,
|
|
||||||
bundler?: string
|
|
||||||
}) => {
|
|
||||||
console.log(`> Building...`);
|
console.log(`> Building...`);
|
||||||
|
|
||||||
process.env.NODE_ENV = process.env.NODE_ENV || 'production';
|
process.env.NODE_ENV = process.env.NODE_ENV || 'production';
|
||||||
@@ -47,7 +30,7 @@ prog.command('build [dest]')
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const { build } = await import('./cli/build');
|
const { build } = await import('./cli/build');
|
||||||
await build(opts);
|
await build();
|
||||||
|
|
||||||
const launcher = path.resolve(dest, 'index.js');
|
const launcher = path.resolve(dest, 'index.js');
|
||||||
|
|
||||||
@@ -58,12 +41,12 @@ prog.command('build [dest]')
|
|||||||
process.env.PORT = process.env.PORT || ${opts.port || 3000};
|
process.env.PORT = process.env.PORT || ${opts.port || 3000};
|
||||||
|
|
||||||
console.log('Starting server on port ' + process.env.PORT);
|
console.log('Starting server on port ' + process.env.PORT);
|
||||||
require('./server/server.js');
|
require('./server.js');
|
||||||
`.replace(/^\t+/gm, '').trim());
|
`.replace(/^\t+/gm, '').trim());
|
||||||
|
|
||||||
console.error(`\n> Finished in ${elapsed(start)}. Type ${colors.bold.cyan(`node ${dest}`)} to run the app.`);
|
console.error(`\n> Finished in ${elapsed(start)}. Type ${colors.bold.cyan(`node ${dest}`)} to run the app.`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(`${colors.bold.red(`> ${err.message}`)}`);
|
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -80,19 +63,9 @@ prog.command('start [dir]')
|
|||||||
prog.command('export [dest]')
|
prog.command('export [dest]')
|
||||||
.describe('Export your app as static files (if possible)')
|
.describe('Export your app as static files (if possible)')
|
||||||
.option('--build', '(Re)build app before exporting', true)
|
.option('--build', '(Re)build app before exporting', true)
|
||||||
.option('--build-dir', 'Specify a custom temporary build directory', '__sapper__/build')
|
.option('--build-dir', 'Specify a custom temporary build directory', '.sapper/prod')
|
||||||
.option('--basepath', 'Specify a base path')
|
.option('--basepath', 'Specify a base path')
|
||||||
.option('--timeout', 'Milliseconds to wait for a page (--no-timeout to disable)', 5000)
|
.action(async (dest = 'export', opts: { build: boolean, 'build-dir': string, basepath?: string }) => {
|
||||||
.option('--legacy', 'Create separate legacy build')
|
|
||||||
.option('--bundler', 'Specify a bundler (rollup or webpack, blank for auto)')
|
|
||||||
.action(async (dest = '__sapper__/export', opts: {
|
|
||||||
build: boolean,
|
|
||||||
legacy: boolean,
|
|
||||||
bundler?: string,
|
|
||||||
'build-dir': string,
|
|
||||||
basepath?: string,
|
|
||||||
timeout: number | false
|
|
||||||
}) => {
|
|
||||||
process.env.NODE_ENV = 'production';
|
process.env.NODE_ENV = 'production';
|
||||||
process.env.SAPPER_DEST = opts['build-dir'];
|
process.env.SAPPER_DEST = opts['build-dir'];
|
||||||
|
|
||||||
@@ -102,7 +75,7 @@ prog.command('export [dest]')
|
|||||||
if (opts.build) {
|
if (opts.build) {
|
||||||
console.log(`> Building...`);
|
console.log(`> Building...`);
|
||||||
const { build } = await import('./cli/build');
|
const { build } = await import('./cli/build');
|
||||||
await build(opts);
|
await build();
|
||||||
console.error(`\n> Built in ${elapsed(start)}`);
|
console.error(`\n> Built in ${elapsed(start)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +83,7 @@ prog.command('export [dest]')
|
|||||||
await exporter(dest, opts);
|
await exporter(dest, opts);
|
||||||
console.error(`\n> Finished in ${elapsed(start)}. Type ${colors.bold.cyan(`npx serve ${dest}`)} to run the app.`);
|
console.error(`\n> Finished in ${elapsed(start)}. Type ${colors.bold.cyan(`npx serve ${dest}`)} to run the app.`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(colors.bold.red(`> ${err.message}`));
|
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,45 +1,20 @@
|
|||||||
import { build as _build } from '../api/build';
|
import { build as _build } from '../api/build';
|
||||||
import colors from 'kleur';
|
import * as colors from 'ansi-colors';
|
||||||
import { locations } from '../config';
|
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`);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export function build() {
|
||||||
return new Promise((fulfil, reject) => {
|
return new Promise((fulfil, reject) => {
|
||||||
try {
|
try {
|
||||||
const emitter = _build({
|
const emitter = _build({
|
||||||
legacy: opts.legacy,
|
|
||||||
bundler
|
|
||||||
}, {
|
|
||||||
dest: locations.dest(),
|
dest: locations.dest(),
|
||||||
src: locations.src(),
|
app: locations.app(),
|
||||||
routes: locations.routes(),
|
routes: locations.routes(),
|
||||||
webpack: 'webpack',
|
webpack: 'webpack'
|
||||||
rollup: 'rollup'
|
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on('build', event => {
|
emitter.on('build', event => {
|
||||||
let banner = `built ${event.type}`;
|
console.log(colors.inverse(`\nbuilt ${event.type}`));
|
||||||
let c = colors.cyan;
|
console.log(event.webpack_stats.toString({ colors: true }));
|
||||||
|
|
||||||
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 => {
|
emitter.on('error', event => {
|
||||||
@@ -50,7 +25,8 @@ export function build(opts: { bundler?: string, legacy?: boolean }) {
|
|||||||
fulfil();
|
fulfil();
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
reject(err);
|
console.log(`${colors.bold.red(`> ${err.message}`)}`);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import colors from 'kleur';
|
import * as colors from 'ansi-colors';
|
||||||
import * as child_process from 'child_process';
|
import * as child_process from 'child_process';
|
||||||
import prettyMs from 'pretty-ms';
|
import prettyMs from 'pretty-ms';
|
||||||
|
import pb from 'pretty-bytes';
|
||||||
import { dev as _dev } from '../api/dev';
|
import { dev as _dev } from '../api/dev';
|
||||||
import * as events from '../api/interfaces';
|
import * as events from '../api/interfaces';
|
||||||
|
|
||||||
export function dev(opts: { port: number, open: boolean, bundler?: string }) {
|
export function dev(opts: { port: number, open: boolean }) {
|
||||||
try {
|
try {
|
||||||
const watcher = _dev(opts);
|
const watcher = _dev(opts);
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ export function dev(opts: { port: number, open: boolean, bundler?: string }) {
|
|||||||
|
|
||||||
watcher.on('ready', (event: events.ReadyEvent) => {
|
watcher.on('ready', (event: events.ReadyEvent) => {
|
||||||
if (first) {
|
if (first) {
|
||||||
console.log(colors.bold.cyan(`> Listening on http://localhost:${event.port}`));
|
console.log(`${colors.bold.cyan(`> Listening on http://localhost:${event.port}`)}`);
|
||||||
if (opts.open) child_process.exec(`open http://localhost:${event.port}`);
|
if (opts.open) child_process.exec(`open http://localhost:${event.port}`);
|
||||||
first = false;
|
first = false;
|
||||||
}
|
}
|
||||||
@@ -35,21 +36,46 @@ export function dev(opts: { port: number, open: boolean, bundler?: string }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
watcher.on('error', (event: events.ErrorEvent) => {
|
watcher.on('error', (event: events.ErrorEvent) => {
|
||||||
console.log(colors.red(`✗ ${event.type}`));
|
console.log(`${colors.red(`✗ ${event.type}`)}`);
|
||||||
console.log(colors.red(event.message));
|
console.log(`${colors.red(event.message)}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
watcher.on('fatal', (event: events.FatalEvent) => {
|
watcher.on('fatal', (event: events.FatalEvent) => {
|
||||||
console.log(colors.bold.red(`> ${event.message}`));
|
console.log(`${colors.bold.red(`> ${event.message}`)}`);
|
||||||
if (event.log) console.log(event.log);
|
if (event.log) console.log(event.log);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watcher.on('preload', (event) => {
|
||||||
|
if (event.size > 25000) {
|
||||||
|
console.log(colors.bold.yellow(`${event.url} — large amount of preloaded data`));
|
||||||
|
console.log(`${colors.bold(pb(event.size))} of data was preloaded in total, above the recommended limit of ${pb(25000)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher.on('unused_data', (event) => {
|
||||||
|
console.log(colors.bold.yellow(`${event.url} — unused preloaded data`));
|
||||||
|
console.log(`More data was returned from \`preload\` than was used during the initial render. Consider only returning essential data.`);
|
||||||
|
|
||||||
|
event.discrepancies.forEach(discrepancy => {
|
||||||
|
console.log(`${colors.bold(discrepancy.file)} loaded ${colors.bold(pb(discrepancy.preloaded))}, of which ${discrepancy.rendered > 2 ? `only ${colors.bold(pb(discrepancy.rendered))}` : 'none'} was used. The following properties were not referenced:`);
|
||||||
|
|
||||||
|
const slice = discrepancy.props.length > 12
|
||||||
|
? discrepancy.props.slice(0, 10)
|
||||||
|
: discrepancy.props;
|
||||||
|
|
||||||
|
console.log(slice.map((prop: string) => `• ${prop}`).join('\n'));
|
||||||
|
|
||||||
|
if (discrepancy.props.length > slice.length) {
|
||||||
|
console.log(`...and ${discrepancy.props.length - slice.length} more`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
watcher.on('build', (event: events.BuildEvent) => {
|
watcher.on('build', (event: events.BuildEvent) => {
|
||||||
if (event.errors.length) {
|
if (event.errors.length) {
|
||||||
console.log(colors.bold.red(`✗ ${event.type}`));
|
console.log(`${colors.bold.red(`✗ ${event.type}`)}`);
|
||||||
|
|
||||||
event.errors.filter(e => !e.duplicate).forEach(error => {
|
event.errors.filter(e => !e.duplicate).forEach(error => {
|
||||||
if (error.file) console.log(colors.bold(error.file));
|
|
||||||
console.log(error.message);
|
console.log(error.message);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,10 +84,9 @@ export function dev(opts: { port: number, open: boolean, bundler?: string }) {
|
|||||||
console.log(`${hidden} duplicate ${hidden === 1 ? 'error' : 'errors'} hidden\n`);
|
console.log(`${hidden} duplicate ${hidden === 1 ? 'error' : 'errors'} hidden\n`);
|
||||||
}
|
}
|
||||||
} else if (event.warnings.length) {
|
} else if (event.warnings.length) {
|
||||||
console.log(colors.bold.yellow(`• ${event.type}`));
|
console.log(`${colors.bold.yellow(`• ${event.type}`)}`);
|
||||||
|
|
||||||
event.warnings.filter(e => !e.duplicate).forEach(warning => {
|
event.warnings.filter(e => !e.duplicate).forEach(warning => {
|
||||||
if (warning.file) console.log(colors.bold(warning.file));
|
|
||||||
console.log(warning.message);
|
console.log(warning.message);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -74,7 +99,7 @@ export function dev(opts: { port: number, open: boolean, bundler?: string }) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(colors.bold.red(`> ${err.message}`));
|
console.log(`${colors.bold.red(`> ${err.message}`)}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,29 +1,26 @@
|
|||||||
import { exporter as _exporter } from '../api/export';
|
import { exporter as _exporter } from '../api/export';
|
||||||
import colors from 'kleur';
|
import * as colors from 'ansi-colors';
|
||||||
import pb from 'pretty-bytes';
|
import prettyBytes from 'pretty-bytes';
|
||||||
import { locations } from '../config';
|
import { locations } from '../config';
|
||||||
import { left_pad } from '../utils';
|
|
||||||
|
|
||||||
export function exporter(export_dir: string, {
|
function left_pad(str: string, len: number) {
|
||||||
basepath = '',
|
while (str.length < len) str = ` ${str}`;
|
||||||
timeout
|
return str;
|
||||||
}: {
|
}
|
||||||
basepath: string,
|
|
||||||
timeout: number | false
|
export function exporter(export_dir: string, { basepath = '' }) {
|
||||||
}) {
|
|
||||||
return new Promise((fulfil, reject) => {
|
return new Promise((fulfil, reject) => {
|
||||||
try {
|
try {
|
||||||
const emitter = _exporter({
|
const emitter = _exporter({
|
||||||
build: locations.dest(),
|
build: locations.dest(),
|
||||||
static: locations.static(),
|
|
||||||
dest: export_dir,
|
dest: export_dir,
|
||||||
basepath,
|
basepath
|
||||||
timeout
|
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on('file', event => {
|
emitter.on('file', event => {
|
||||||
|
const pb = prettyBytes(event.size);
|
||||||
const size_color = event.size > 150000 ? colors.bold.red : event.size > 50000 ? colors.bold.yellow : colors.bold.gray;
|
const size_color = event.size > 150000 ? colors.bold.red : event.size > 50000 ? colors.bold.yellow : colors.bold.gray;
|
||||||
const size_label = size_color(left_pad(pb(event.size), 10));
|
const size_label = size_color(left_pad(prettyBytes(event.size), 10));
|
||||||
|
|
||||||
const file_label = event.status === 200
|
const file_label = event.status === 200
|
||||||
? event.file
|
? event.file
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as child_process from 'child_process';
|
import * as child_process from 'child_process';
|
||||||
import colors from 'kleur';
|
import * as colors from 'ansi-colors';
|
||||||
import * as ports from 'port-authority';
|
import * as ports from 'port-authority';
|
||||||
|
|
||||||
export async function start(dir: string, opts: { port: number, open: boolean }) {
|
export async function start(dir: string, opts: { port: number, open: boolean }) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import colors from 'kleur';
|
import * as colors from 'ansi-colors';
|
||||||
|
|
||||||
export default async function upgrade() {
|
export default async function upgrade() {
|
||||||
const upgraded = [
|
const upgraded = [
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
import * as fs from 'fs';
|
|
||||||
|
|
||||||
export default function validate_bundler(bundler?: 'rollup' | 'webpack') {
|
|
||||||
if (!bundler) {
|
|
||||||
bundler = (
|
|
||||||
fs.existsSync('rollup.config.js') ? 'rollup' :
|
|
||||||
fs.existsSync('webpack.config.js') ? 'webpack' :
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!bundler) {
|
|
||||||
// TODO remove in a future version
|
|
||||||
deprecate_dir('rollup');
|
|
||||||
deprecate_dir('webpack');
|
|
||||||
|
|
||||||
throw new Error(`Could not find rollup.config.js or webpack.config.js`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bundler !== 'rollup' && bundler !== 'webpack') {
|
|
||||||
throw new Error(`'${bundler}' is not a valid option for --bundler — must be either 'rollup' or 'webpack'`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return bundler;
|
|
||||||
}
|
|
||||||
|
|
||||||
function deprecate_dir(bundler: 'rollup' | 'webpack') {
|
|
||||||
try {
|
|
||||||
const stats = fs.statSync(bundler);
|
|
||||||
if (!stats.isDirectory()) return;
|
|
||||||
} catch (err) {
|
|
||||||
// do nothing
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO link to docs, once those docs exist
|
|
||||||
throw new Error(`As of Sapper 0.21, build configuration should be placed in a single ${bundler}.config.js file`);
|
|
||||||
}
|
|
||||||
@@ -4,8 +4,7 @@ export const dev = () => process.env.NODE_ENV !== 'production';
|
|||||||
|
|
||||||
export const locations = {
|
export const locations = {
|
||||||
base: () => path.resolve(process.env.SAPPER_BASE || ''),
|
base: () => path.resolve(process.env.SAPPER_BASE || ''),
|
||||||
src: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_SRC || 'src'),
|
app: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_APP || 'app'),
|
||||||
static: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_STATIC || 'static'),
|
routes: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_ROUTES || 'routes'),
|
||||||
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' : 'prod'}`)
|
||||||
dest: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_DEST || `__sapper__/${dev() ? 'dev' : 'build'}`)
|
|
||||||
};
|
};
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
export * from './core/create_manifests';
|
export * from './core/create_manifests';
|
||||||
export { default as create_compilers } from './core/create_compilers/index';
|
export { default as create_compilers } from './core/create_compilers';
|
||||||
export { default as create_manifest_data } from './core/create_manifest_data';
|
export { default as create_routes } from './core/create_routes';
|
||||||
29
src/core/create_compilers.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import * as path from 'path';
|
||||||
|
import relative from 'require-relative';
|
||||||
|
|
||||||
|
export default function create_compilers({ webpack }: { webpack: string }) {
|
||||||
|
const wp = relative('webpack', process.cwd());
|
||||||
|
|
||||||
|
const serviceworker_config = try_require(path.resolve(`${webpack}/service-worker.config.js`));
|
||||||
|
|
||||||
|
return {
|
||||||
|
client: wp(
|
||||||
|
require(path.resolve(`${webpack}/client.config.js`))
|
||||||
|
),
|
||||||
|
|
||||||
|
server: wp(
|
||||||
|
require(path.resolve(`${webpack}/server.config.js`))
|
||||||
|
),
|
||||||
|
|
||||||
|
serviceworker: serviceworker_config && wp(serviceworker_config)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function try_require(specifier: string) {
|
||||||
|
try {
|
||||||
|
return require(specifier);
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === 'MODULE_NOT_FOUND') return null;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
import * as path from 'path';
|
|
||||||
import relative from 'require-relative';
|
|
||||||
import { CompileResult } from './interfaces';
|
|
||||||
import RollupResult from './RollupResult';
|
|
||||||
|
|
||||||
let rollup: any;
|
|
||||||
|
|
||||||
export default class RollupCompiler {
|
|
||||||
_: Promise<any>;
|
|
||||||
_oninvalid: (filename: string) => void;
|
|
||||||
_start: number;
|
|
||||||
input: string;
|
|
||||||
warnings: any[];
|
|
||||||
errors: any[];
|
|
||||||
chunks: any[];
|
|
||||||
css_files: Array<{ id: string, code: string }>;
|
|
||||||
|
|
||||||
constructor(config: any) {
|
|
||||||
this._ = this.get_config(config);
|
|
||||||
this.input = null;
|
|
||||||
this.warnings = [];
|
|
||||||
this.errors = [];
|
|
||||||
this.chunks = [];
|
|
||||||
this.css_files = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async get_config(mod: any) {
|
|
||||||
// TODO this is hacky, and doesn't need to apply to all three compilers
|
|
||||||
(mod.plugins || (mod.plugins = [])).push({
|
|
||||||
name: 'sapper-internal',
|
|
||||||
options: (opts: any) => {
|
|
||||||
this.input = opts.input;
|
|
||||||
},
|
|
||||||
renderChunk: (code: string, chunk: any) => {
|
|
||||||
this.chunks.push(chunk);
|
|
||||||
},
|
|
||||||
transform: (code: string, id: string) => {
|
|
||||||
if (/\.css$/.test(id)) {
|
|
||||||
this.css_files.push({ id, code });
|
|
||||||
return ``;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const onwarn = mod.onwarn || ((warning: any, handler: (warning: any) => void) => {
|
|
||||||
handler(warning);
|
|
||||||
});
|
|
||||||
|
|
||||||
mod.onwarn = (warning: any) => {
|
|
||||||
onwarn(warning, (warning: any) => {
|
|
||||||
this.warnings.push(warning);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return mod;
|
|
||||||
}
|
|
||||||
|
|
||||||
oninvalid(cb: (filename: string) => void) {
|
|
||||||
this._oninvalid = cb;
|
|
||||||
}
|
|
||||||
|
|
||||||
async compile(): Promise<CompileResult> {
|
|
||||||
const config = await this._;
|
|
||||||
|
|
||||||
const start = Date.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const bundle = await rollup.rollup(config);
|
|
||||||
await bundle.write(config.output);
|
|
||||||
|
|
||||||
return new RollupResult(Date.now() - start, this);
|
|
||||||
} catch (err) {
|
|
||||||
if (err.filename) {
|
|
||||||
// TODO this is a bit messy. Also, can
|
|
||||||
// Rollup emit other kinds of error?
|
|
||||||
err.message = [
|
|
||||||
`Failed to build — error in ${err.filename}: ${err.message}`,
|
|
||||||
err.frame
|
|
||||||
].filter(Boolean).join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async watch(cb: (err?: Error, stats?: any) => void) {
|
|
||||||
const config = await this._;
|
|
||||||
|
|
||||||
const watcher = rollup.watch(config);
|
|
||||||
|
|
||||||
watcher.on('change', (id: string) => {
|
|
||||||
this.chunks = [];
|
|
||||||
this.warnings = [];
|
|
||||||
this.errors = [];
|
|
||||||
this._oninvalid(id);
|
|
||||||
});
|
|
||||||
|
|
||||||
watcher.on('event', (event: any) => {
|
|
||||||
switch (event.code) {
|
|
||||||
case 'FATAL':
|
|
||||||
// TODO kill the process?
|
|
||||||
if (event.error.filename) {
|
|
||||||
// TODO this is a bit messy. Also, can
|
|
||||||
// Rollup emit other kinds of error?
|
|
||||||
event.error.message = [
|
|
||||||
`Failed to build — error in ${event.error.filename}: ${event.error.message}`,
|
|
||||||
event.error.frame
|
|
||||||
].filter(Boolean).join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
cb(event.error);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'ERROR':
|
|
||||||
this.errors.push(event.error);
|
|
||||||
cb(null, new RollupResult(Date.now() - this._start, this));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'START':
|
|
||||||
case 'END':
|
|
||||||
// TODO is there anything to do with this info?
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'BUNDLE_START':
|
|
||||||
this._start = Date.now();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'BUNDLE_END':
|
|
||||||
cb(null, new RollupResult(Date.now() - this._start, this));
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
console.log(`Unexpected event ${event.code}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static async load_config() {
|
|
||||||
if (!rollup) rollup = relative('rollup', process.cwd());
|
|
||||||
|
|
||||||
const input = path.resolve('rollup.config.js');
|
|
||||||
|
|
||||||
const bundle = await rollup.rollup({
|
|
||||||
input,
|
|
||||||
external: (id: string) => {
|
|
||||||
return (id[0] !== '.' && !path.isAbsolute(id)) || id.slice(-5, id.length) === '.json';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const { code } = await bundle.generate({ format: 'cjs' });
|
|
||||||
|
|
||||||
// temporarily override require
|
|
||||||
const defaultLoader = require.extensions['.js'];
|
|
||||||
require.extensions['.js'] = (module: any, filename: string) => {
|
|
||||||
if (filename === input) {
|
|
||||||
module._compile(code, filename);
|
|
||||||
} else {
|
|
||||||
defaultLoader(module, filename);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const config: any = require(input);
|
|
||||||
delete require.cache[input];
|
|
||||||
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
import * as path from 'path';
|
|
||||||
import colors from 'kleur';
|
|
||||||
import pb from 'pretty-bytes';
|
|
||||||
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';
|
|
||||||
|
|
||||||
export default class RollupResult implements CompileResult {
|
|
||||||
duration: number;
|
|
||||||
errors: CompileError[];
|
|
||||||
warnings: CompileError[];
|
|
||||||
chunks: Chunk[];
|
|
||||||
assets: Record<string, string>;
|
|
||||||
css_files: CssFile[];
|
|
||||||
css: {
|
|
||||||
main: string,
|
|
||||||
chunks: Record<string, string[]>
|
|
||||||
};
|
|
||||||
summary: string;
|
|
||||||
|
|
||||||
constructor(duration: number, compiler: RollupCompiler) {
|
|
||||||
this.duration = duration;
|
|
||||||
|
|
||||||
this.errors = compiler.errors.map(munge_warning_or_error);
|
|
||||||
this.warnings = compiler.warnings.map(munge_warning_or_error); // TODO emit this as they happen
|
|
||||||
|
|
||||||
this.chunks = compiler.chunks.map(chunk => ({
|
|
||||||
file: chunk.fileName,
|
|
||||||
imports: chunk.imports.filter(Boolean),
|
|
||||||
modules: Object.keys(chunk.modules)
|
|
||||||
}));
|
|
||||||
|
|
||||||
this.css_files = compiler.css_files;
|
|
||||||
|
|
||||||
// TODO populate this properly. We don't have named chunks, as in
|
|
||||||
// webpack, but we can have a route -> [chunk] map or something
|
|
||||||
this.assets = {};
|
|
||||||
|
|
||||||
if (typeof compiler.input === 'string') {
|
|
||||||
compiler.chunks.forEach(chunk => {
|
|
||||||
if (compiler.input in chunk.modules) {
|
|
||||||
this.assets.main = chunk.fileName;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
for (const name in compiler.input) {
|
|
||||||
const file = compiler.input[name];
|
|
||||||
this.assets[name] = compiler.chunks.find(chunk => file in chunk.modules).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_label = left_pad(pb(chunk.code.length), 10);
|
|
||||||
|
|
||||||
const lines = [size_color(`${size_label} ${chunk.fileName}`)];
|
|
||||||
|
|
||||||
const deps = Object.keys(chunk.modules)
|
|
||||||
.map(file => {
|
|
||||||
return {
|
|
||||||
file: path.relative(process.cwd(), file),
|
|
||||||
size: chunk.modules[file].renderedLength
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter(dep => dep.size > 0)
|
|
||||||
.sort((a, b) => b.size - a.size);
|
|
||||||
|
|
||||||
const total_unminified = deps.reduce((t, d) => t + d.size, 0);
|
|
||||||
|
|
||||||
deps.forEach((dep, i) => {
|
|
||||||
const c = i === deps.length - 1 ? '└' : '│';
|
|
||||||
let line = ` ${c} ${dep.file}`;
|
|
||||||
|
|
||||||
if (deps.length > 1) {
|
|
||||||
const p = (100 * dep.size / total_unminified).toFixed(1);
|
|
||||||
line += ` (${p}%)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
lines.push(colors.gray(line));
|
|
||||||
});
|
|
||||||
|
|
||||||
return lines.join('\n');
|
|
||||||
}).join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
to_json(manifest_data: ManifestData, dirs: Dirs): BuildInfo {
|
|
||||||
// TODO extract_css has side-effects that don't belong
|
|
||||||
// in a method called to_json
|
|
||||||
return {
|
|
||||||
bundler: 'rollup',
|
|
||||||
shimport: require('shimport/package.json').version,
|
|
||||||
assets: this.assets,
|
|
||||||
css: extract_css(this, manifest_data.components, dirs)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
print() {
|
|
||||||
const blocks: string[] = this.warnings.map(warning => {
|
|
||||||
return warning.file
|
|
||||||
? `> ${colors.bold(warning.file)}\n${warning.message}`
|
|
||||||
: `> ${warning.message}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
blocks.push(this.summary);
|
|
||||||
|
|
||||||
return blocks.join('\n\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function munge_warning_or_error(warning_or_error: any) {
|
|
||||||
return {
|
|
||||||
file: warning_or_error.filename,
|
|
||||||
message: [warning_or_error.message, warning_or_error.frame].filter(Boolean).join('\n')
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import relative from 'require-relative';
|
|
||||||
import { CompileResult } from './interfaces';
|
|
||||||
import WebpackResult from './WebpackResult';
|
|
||||||
|
|
||||||
let webpack: any;
|
|
||||||
|
|
||||||
export class WebpackCompiler {
|
|
||||||
_: any;
|
|
||||||
|
|
||||||
constructor(config: any) {
|
|
||||||
if (!webpack) webpack = relative('webpack', process.cwd());
|
|
||||||
this._ = webpack(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
oninvalid(cb: (filename: string) => void) {
|
|
||||||
this._.hooks.invalid.tap('sapper', cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
compile(): Promise<CompileResult> {
|
|
||||||
return new Promise((fulfil, reject) => {
|
|
||||||
this._.run((err: Error, stats: any) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = new WebpackResult(stats);
|
|
||||||
|
|
||||||
if (result.errors.length) {
|
|
||||||
console.error(stats.toString({ colors: true }));
|
|
||||||
reject(new Error(`Encountered errors while building app`));
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
fulfil(result);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(cb: (err?: Error, stats?: any) => void) {
|
|
||||||
this._.watch({}, (err?: Error, stats?: any) => {
|
|
||||||
cb(err, stats && new WebpackResult(stats));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import format_messages from 'webpack-format-messages';
|
|
||||||
import { CompileResult, BuildInfo, CompileError, Chunk, CssFile } from './interfaces';
|
|
||||||
import { ManifestData, Dirs } from '../../interfaces';
|
|
||||||
|
|
||||||
const locPattern = /\((\d+):(\d+)\)$/;
|
|
||||||
|
|
||||||
function munge_warning_or_error(message: string) {
|
|
||||||
// TODO this is all a bit rube goldberg...
|
|
||||||
const lines = message.split('\n');
|
|
||||||
|
|
||||||
const file = lines.shift()
|
|
||||||
.replace('[7m', '') // careful — there is a special character at the beginning of this string
|
|
||||||
.replace('[27m', '')
|
|
||||||
.replace('./', '');
|
|
||||||
|
|
||||||
let line = null;
|
|
||||||
let column = null;
|
|
||||||
|
|
||||||
const match = locPattern.exec(lines[0]);
|
|
||||||
if (match) {
|
|
||||||
lines[0] = lines[0].replace(locPattern, '');
|
|
||||||
line = +match[1];
|
|
||||||
column = +match[2];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
file,
|
|
||||||
message: lines.join('\n')
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class WebpackResult implements CompileResult {
|
|
||||||
duration: number;
|
|
||||||
errors: CompileError[];
|
|
||||||
warnings: CompileError[];
|
|
||||||
chunks: Chunk[];
|
|
||||||
assets: Record<string, string>;
|
|
||||||
css_files: CssFile[];
|
|
||||||
stats: any;
|
|
||||||
|
|
||||||
constructor(stats: any) {
|
|
||||||
this.stats = stats;
|
|
||||||
|
|
||||||
const info = stats.toJson();
|
|
||||||
|
|
||||||
const messages = format_messages(stats);
|
|
||||||
|
|
||||||
this.errors = messages.errors.map(munge_warning_or_error);
|
|
||||||
this.warnings = messages.warnings.map(munge_warning_or_error);
|
|
||||||
|
|
||||||
this.duration = info.time;
|
|
||||||
|
|
||||||
this.chunks = info.assets.map((chunk: { name: string }) => ({ file: chunk.name }));
|
|
||||||
this.assets = info.assetsByChunkName;
|
|
||||||
}
|
|
||||||
|
|
||||||
to_json(manifest_data: ManifestData, dirs: Dirs): BuildInfo {
|
|
||||||
return {
|
|
||||||
bundler: 'webpack',
|
|
||||||
shimport: null, // webpack has its own loader
|
|
||||||
assets: this.assets,
|
|
||||||
css: {
|
|
||||||
// TODO
|
|
||||||
main: null,
|
|
||||||
chunks: {}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
print() {
|
|
||||||
return this.stats.toString({ colors: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
import * as fs from 'fs';
|
|
||||||
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'
|
|
||||||
|
|
||||||
const inline_sourcemap_header = 'data:application/json;charset=utf-8;base64,';
|
|
||||||
|
|
||||||
function extract_sourcemap(raw: string, id: string) {
|
|
||||||
let raw_map: string;
|
|
||||||
let map = null;
|
|
||||||
|
|
||||||
const code = raw.replace(/\/\*#\s+sourceMappingURL=(.+)\s+\*\//g, (m, url) => {
|
|
||||||
if (raw_map) {
|
|
||||||
// TODO should not happen!
|
|
||||||
throw new Error(`Found multiple sourcemaps in single CSS file (${id})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
raw_map = url;
|
|
||||||
return '';
|
|
||||||
}).trim();
|
|
||||||
|
|
||||||
if (raw_map) {
|
|
||||||
if (raw_map.startsWith(inline_sourcemap_header)) {
|
|
||||||
const json = Buffer.from(raw_map.slice(inline_sourcemap_header.length), 'base64').toString();
|
|
||||||
map = JSON.parse(json);
|
|
||||||
} else {
|
|
||||||
// TODO do we want to handle non-inline sourcemaps? could be a rabbit hole
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
code,
|
|
||||||
map
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
type SourceMap = {
|
|
||||||
version: 3;
|
|
||||||
file: string;
|
|
||||||
sources: string[];
|
|
||||||
sourcesContent: string[];
|
|
||||||
names: string[];
|
|
||||||
mappings: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function extract_css(client_result: CompileResult, components: PageComponent[], dirs: Dirs) {
|
|
||||||
const result: {
|
|
||||||
main: string | null;
|
|
||||||
chunks: Record<string, string[]>
|
|
||||||
} = {
|
|
||||||
main: null,
|
|
||||||
chunks: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!client_result.css_files) return; // Rollup-only for now
|
|
||||||
|
|
||||||
const unaccounted_for = new Set();
|
|
||||||
|
|
||||||
const css_map = new Map();
|
|
||||||
client_result.css_files.forEach(css => {
|
|
||||||
unaccounted_for.add(css.id);
|
|
||||||
css_map.set(css.id, css.code);
|
|
||||||
});
|
|
||||||
|
|
||||||
const chunk_map = new Map();
|
|
||||||
client_result.chunks.forEach(chunk => {
|
|
||||||
chunk_map.set(chunk.file, chunk);
|
|
||||||
});
|
|
||||||
|
|
||||||
const chunks_with_css = new Set();
|
|
||||||
|
|
||||||
// figure out which chunks belong to which components...
|
|
||||||
const component_owners = new Map();
|
|
||||||
client_result.chunks.forEach(chunk => {
|
|
||||||
chunk.modules.forEach(module => {
|
|
||||||
const component = posixify(path.relative(dirs.routes, module));
|
|
||||||
component_owners.set(component, chunk);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const chunks_depended_upon_by_component = new Map();
|
|
||||||
|
|
||||||
// ...so we can figure out which chunks don't belong
|
|
||||||
components.forEach(component => {
|
|
||||||
const chunk = component_owners.get(component.file);
|
|
||||||
if (!chunk) {
|
|
||||||
// this should never happen!
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
chunks.forEach(chunk => {
|
|
||||||
chunk.modules.forEach((module: string) => {
|
|
||||||
unaccounted_for.delete(module);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
let asset_dir = `${dirs.dest}/client`;
|
|
||||||
if (process.env.SAPPER_LEGACY_BUILD) asset_dir += '/legacy';
|
|
||||||
|
|
||||||
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(`${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, ' '));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const files = chunks_with_css.map(chunk => chunk.file.replace(/\.js$/, '.css'));
|
|
||||||
|
|
||||||
replacements.set(
|
|
||||||
component.file,
|
|
||||||
files
|
|
||||||
);
|
|
||||||
|
|
||||||
result.chunks[component.file] = files;
|
|
||||||
});
|
|
||||||
|
|
||||||
fs.readdirSync(asset_dir).forEach(file => {
|
|
||||||
if (fs.statSync(`${asset_dir}/${file}`).isDirectory()) return;
|
|
||||||
|
|
||||||
const source = fs.readFileSync(`${asset_dir}/${file}`, 'utf-8');
|
|
||||||
|
|
||||||
const replaced = source.replace(/["']__SAPPER_CSS_PLACEHOLDER:(.+?)__["']/g, (m, route) => {
|
|
||||||
return JSON.stringify(replacements.get(route));
|
|
||||||
});
|
|
||||||
|
|
||||||
fs.writeFileSync(`${asset_dir}/${file}`, replaced);
|
|
||||||
});
|
|
||||||
|
|
||||||
const leftover = get_css_from_modules(Array.from(unaccounted_for));
|
|
||||||
if (leftover) {
|
|
||||||
const { code, map } = leftover;
|
|
||||||
|
|
||||||
const main_hash = hash(code);
|
|
||||||
|
|
||||||
const output_file_name = `main.${main_hash}.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=client/${output_file_name}.map */`);
|
|
||||||
fs.writeFileSync(`${asset_dir}/${output_file_name}.map`, JSON.stringify(map, null, ' '));
|
|
||||||
|
|
||||||
result.main = output_file_name;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import * as path from 'path';
|
|
||||||
import RollupCompiler from './RollupCompiler';
|
|
||||||
import { WebpackCompiler } from './WebpackCompiler';
|
|
||||||
|
|
||||||
export type Compiler = RollupCompiler | WebpackCompiler;
|
|
||||||
|
|
||||||
export type Compilers = {
|
|
||||||
client: Compiler;
|
|
||||||
server: Compiler;
|
|
||||||
serviceworker?: Compiler;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function create_compilers(bundler: 'rollup' | 'webpack'): Promise<Compilers> {
|
|
||||||
if (bundler === 'rollup') {
|
|
||||||
const config = await RollupCompiler.load_config();
|
|
||||||
validate_config(config, 'rollup');
|
|
||||||
|
|
||||||
normalize_rollup_config(config.client);
|
|
||||||
normalize_rollup_config(config.server);
|
|
||||||
|
|
||||||
if (config.serviceworker) {
|
|
||||||
normalize_rollup_config(config.serviceworker);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
client: new RollupCompiler(config.client),
|
|
||||||
server: new RollupCompiler(config.server),
|
|
||||||
serviceworker: config.serviceworker && new RollupCompiler(config.serviceworker)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bundler === 'webpack') {
|
|
||||||
const config = require(path.resolve('webpack.config.js'));
|
|
||||||
validate_config(config, 'webpack');
|
|
||||||
|
|
||||||
return {
|
|
||||||
client: new WebpackCompiler(config.client),
|
|
||||||
server: new WebpackCompiler(config.server),
|
|
||||||
serviceworker: config.serviceworker && new WebpackCompiler(config.serviceworker)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// this shouldn't be possible...
|
|
||||||
throw new Error(`Invalid bundler option '${bundler}'`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function validate_config(config: any, bundler: 'rollup' | 'webpack') {
|
|
||||||
if (!config.client || !config.server) {
|
|
||||||
throw new Error(`${bundler}.config.js must export a { client, server, serviceworker? } object`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalize_rollup_config(config: any) {
|
|
||||||
if (typeof config.input === 'string') {
|
|
||||||
config.input = path.normalize(config.input);
|
|
||||||
} else {
|
|
||||||
for (const name in config.input) {
|
|
||||||
config.input[name] = path.normalize(config.input[name]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { ManifestData, Dirs } from '../../interfaces';
|
|
||||||
|
|
||||||
export type Chunk = {
|
|
||||||
file: string;
|
|
||||||
imports: string[];
|
|
||||||
modules: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CssFile = {
|
|
||||||
id: string;
|
|
||||||
code: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class CompileError {
|
|
||||||
file: string;
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CompileResult {
|
|
||||||
duration: number;
|
|
||||||
errors: CompileError[];
|
|
||||||
warnings: CompileError[];
|
|
||||||
chunks: Chunk[];
|
|
||||||
assets: Record<string, string>;
|
|
||||||
css_files: CssFile[];
|
|
||||||
|
|
||||||
to_json: (manifest_data: ManifestData, dirs: Dirs) => BuildInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BuildInfo = {
|
|
||||||
bundler: string;
|
|
||||||
shimport: string;
|
|
||||||
assets: Record<string, string>;
|
|
||||||
legacy_assets?: Record<string, string>;
|
|
||||||
css: {
|
|
||||||
main: string | null,
|
|
||||||
chunks: Record<string, string[]>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,183 +1,153 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import glob from 'tiny-glob/sync.js';
|
import * as glob from 'glob';
|
||||||
import { posixify, stringify, write_if_changed } from './utils';
|
import { posixify, write_if_changed } from './utils';
|
||||||
import { dev, locations } from '../config';
|
import { dev, locations } from '../config';
|
||||||
import { Page, PageComponent, ServerRoute, ManifestData } from '../interfaces';
|
import { Page, PageComponent, ServerRoute } from '../interfaces';
|
||||||
|
|
||||||
export function create_main_manifests({ bundler, manifest_data, dev_port }: {
|
export function create_main_manifests({ routes, dev_port }: {
|
||||||
bundler: string,
|
routes: { components: PageComponent[], pages: Page[], server_routes: ServerRoute[] };
|
||||||
manifest_data: ManifestData;
|
|
||||||
dev_port?: number;
|
dev_port?: number;
|
||||||
}) {
|
}) {
|
||||||
const manifest_dir = '__sapper__';
|
const path_to_routes = path.relative(`${locations.app()}/manifest`, locations.routes());
|
||||||
if (!fs.existsSync(manifest_dir)) fs.mkdirSync(manifest_dir);
|
|
||||||
|
|
||||||
const path_to_routes = path.relative(manifest_dir, locations.routes());
|
const client_manifest = generate_client(routes, path_to_routes, dev_port);
|
||||||
|
const server_manifest = generate_server(routes, path_to_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(
|
write_if_changed(
|
||||||
`${manifest_dir}/default-layout.html`,
|
`${locations.app()}/manifest/default-layout.html`,
|
||||||
`<svelte:component this={child.component} {...child.props}/>`
|
`<svelte:component this={child.component} {...child.props}/>`
|
||||||
);
|
);
|
||||||
write_if_changed(`${manifest_dir}/client.js`, client_manifest);
|
write_if_changed(`${locations.app()}/manifest/client.js`, client_manifest);
|
||||||
write_if_changed(`${manifest_dir}/server.js`, server_manifest);
|
write_if_changed(`${locations.app()}/manifest/server.js`, server_manifest);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function create_serviceworker_manifest({ manifest_data, client_files }: {
|
export function create_serviceworker_manifest({ routes, client_files }: {
|
||||||
manifest_data: ManifestData;
|
routes: { components: PageComponent[], pages: Page[], server_routes: ServerRoute[] };
|
||||||
client_files: string[];
|
client_files: string[];
|
||||||
}) {
|
}) {
|
||||||
let files;
|
const assets = glob.sync('**', { cwd: 'assets', nodir: true });
|
||||||
|
|
||||||
// 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 = `
|
let code = `
|
||||||
// This file is generated by Sapper — do not edit it!
|
// This file is generated by Sapper — do not edit it!
|
||||||
export const timestamp = ${Date.now()};
|
export const timestamp = ${Date.now()};
|
||||||
|
|
||||||
export const files = [\n\t${files.map((x: string) => stringify(x)).join(',\n\t')}\n];
|
export const assets = [\n\t${assets.map((x: string) => `"${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 shell = [\n\t${client_files.map((x: string) => `"${x}"`).join(',\n\t')}\n];
|
||||||
|
|
||||||
export const routes = [\n\t${manifest_data.pages.map((r: Page) => `{ pattern: ${r.pattern} }`).join(',\n\t')}\n];
|
export const routes = [\n\t${routes.pages.map((r: Page) => `{ pattern: ${r.pattern} }`).join(',\n\t')}\n];
|
||||||
`.replace(/^\t\t/gm, '').trim();
|
`.replace(/^\t\t/gm, '').trim();
|
||||||
|
|
||||||
write_if_changed(`__sapper__/service-worker.js`, code);
|
write_if_changed(`${locations.app()}/manifest/service-worker.js`, code);
|
||||||
|
}
|
||||||
|
|
||||||
|
function right_pad(str: string, len: number) {
|
||||||
|
while (str.length < len) str += ' ';
|
||||||
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
function generate_client(
|
function generate_client(
|
||||||
manifest_data: ManifestData,
|
routes: { root: PageComponent, components: PageComponent[], pages: Page[], server_routes: ServerRoute[] },
|
||||||
path_to_routes: string,
|
path_to_routes: string,
|
||||||
bundler: string,
|
|
||||||
dev_port?: number
|
dev_port?: number
|
||||||
) {
|
) {
|
||||||
const template_file = path.resolve(__dirname, '../templates/client.js');
|
const page_ids = new Set(routes.pages.map(page =>
|
||||||
const template = fs.readFileSync(template_file, 'utf-8');
|
|
||||||
|
|
||||||
const page_ids = new Set(manifest_data.pages.map(page =>
|
|
||||||
page.pattern.toString()));
|
page.pattern.toString()));
|
||||||
|
|
||||||
const server_routes_to_ignore = manifest_data.server_routes.filter(route =>
|
const server_routes_to_ignore = routes.server_routes.filter(route =>
|
||||||
!page_ids.has(route.pattern.toString()));
|
!page_ids.has(route.pattern.toString()));
|
||||||
|
|
||||||
const component_indexes: Record<string, number> = {};
|
const len = Math.max(...routes.components.map(c => c.name.length));
|
||||||
|
|
||||||
const components = `[
|
let code = `
|
||||||
${manifest_data.components.map((component, i) => {
|
// This file is generated by Sapper — do not edit it!
|
||||||
const annotation = bundler === 'webpack'
|
import root from '${get_file(path_to_routes, routes.root)}';
|
||||||
? `/* webpackChunkName: "${component.name}" */ `
|
import error from '${posixify(`${path_to_routes}/_error.html`)}';
|
||||||
: '';
|
|
||||||
|
|
||||||
const source = get_file(path_to_routes, component);
|
${routes.components.map(component =>
|
||||||
|
`const ${component.name} = () =>
|
||||||
|
import(/* webpackChunkName: "${component.name}" */ '${get_file(path_to_routes, component)}');`)
|
||||||
|
.join('\n')}
|
||||||
|
|
||||||
component_indexes[component.name] = i;
|
export const manifest = {
|
||||||
|
ignore: [${server_routes_to_ignore.map(route => route.pattern).join(', ')}],
|
||||||
|
|
||||||
return `{
|
pages: [
|
||||||
js: () => import(${annotation}${stringify(source)}),
|
${routes.pages.map(page => `{
|
||||||
css: "__SAPPER_CSS_PLACEHOLDER:${stringify(component.file, false)}__"
|
// ${page.parts[page.parts.length - 1].component.file}
|
||||||
}`;
|
pattern: ${page.pattern},
|
||||||
}).join(',\n\t\t')}
|
parts: [
|
||||||
]`.replace(/^\t/gm, '').trim();
|
${page.parts.map(part => {
|
||||||
|
if (part === null) return 'null';
|
||||||
|
|
||||||
let needs_decode = false;
|
if (part.params.length > 0) {
|
||||||
|
const props = part.params.map((param, i) => `${param}: match[${i + 1}]`);
|
||||||
|
return `{ component: ${part.component.name}, params: match => ({ ${props.join(', ')} }) }`;
|
||||||
|
}
|
||||||
|
|
||||||
let pages = `[
|
return `{ component: ${part.component.name} }`;
|
||||||
${manifest_data.pages.map(page => `{
|
}).join(',\n\t\t\t\t\t\t')}
|
||||||
// ${page.parts[page.parts.length - 1].component.file}
|
]
|
||||||
pattern: ${page.pattern},
|
}`).join(',\n\n\t\t\t\t')}
|
||||||
parts: [
|
],
|
||||||
${page.parts.map(part => {
|
|
||||||
if (part === null) return 'null';
|
|
||||||
|
|
||||||
if (part.params.length > 0) {
|
root,
|
||||||
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]} }`;
|
error
|
||||||
}).join(',\n\t\t\t\t')}
|
};
|
||||||
]
|
|
||||||
}`).join(',\n\n\t\t')}
|
|
||||||
]`.replace(/^\t/gm, '').trim();
|
|
||||||
|
|
||||||
if (needs_decode) {
|
// this is included for legacy reasons
|
||||||
pages = `(d => ${pages})(decodeURIComponent)`
|
export const routes = {};`.replace(/^\t\t/gm, '').trim();
|
||||||
}
|
|
||||||
|
|
||||||
let footer = '';
|
|
||||||
|
|
||||||
if (dev()) {
|
if (dev()) {
|
||||||
const sapper_dev_client = posixify(
|
const sapper_dev_client = posixify(
|
||||||
path.resolve(__dirname, '../sapper-dev-client.js')
|
path.resolve(__dirname, '../sapper-dev-client.js')
|
||||||
);
|
);
|
||||||
|
|
||||||
footer = `
|
code += `
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (module.hot) {
|
||||||
import(${stringify(sapper_dev_client)}).then(client => {
|
import('${sapper_dev_client}').then(client => {
|
||||||
client.connect(${dev_port});
|
client.connect(${dev_port});
|
||||||
});
|
});
|
||||||
}`.replace(/^\t{3}/gm, '');
|
}`.replace(/^\t{3}/gm, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
return `// This file is generated by Sapper — do not edit it!\n` + template
|
return code;
|
||||||
.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(
|
function generate_server(
|
||||||
manifest_data: ManifestData,
|
routes: { root: PageComponent, components: PageComponent[], pages: Page[], server_routes: ServerRoute[] },
|
||||||
path_to_routes: string
|
path_to_routes: string
|
||||||
) {
|
) {
|
||||||
const template_file = path.resolve(__dirname, '../templates/server.js');
|
|
||||||
const template = fs.readFileSync(template_file, 'utf-8');
|
|
||||||
|
|
||||||
const imports = [].concat(
|
const imports = [].concat(
|
||||||
manifest_data.server_routes.map(route =>
|
routes.server_routes.map(route =>
|
||||||
`import * as __${route.name} from ${stringify(posixify(`${path_to_routes}/${route.file}`))};`),
|
`import * as ${route.name} from '${posixify(`${path_to_routes}/${route.file}`)}';`),
|
||||||
manifest_data.components.map(component =>
|
routes.components.map(component =>
|
||||||
`import __${component.name} from ${stringify(get_file(path_to_routes, component))};`),
|
`import ${component.name} from '${get_file(path_to_routes, component)}';`),
|
||||||
`import root from ${stringify(get_file(path_to_routes, manifest_data.root))};`,
|
`import root from '${get_file(path_to_routes, routes.root)}';`,
|
||||||
`import error from ${stringify(posixify(`${path_to_routes}/_error.html`))};`
|
`import error from '${posixify(`${path_to_routes}/_error.html`)}';`
|
||||||
);
|
);
|
||||||
|
|
||||||
let code = `
|
let code = `
|
||||||
|
// This file is generated by Sapper — do not edit it!
|
||||||
${imports.join('\n')}
|
${imports.join('\n')}
|
||||||
|
|
||||||
const d = decodeURIComponent;
|
|
||||||
|
|
||||||
export const manifest = {
|
export const manifest = {
|
||||||
server_routes: [
|
server_routes: [
|
||||||
${manifest_data.server_routes.map(route => `{
|
${routes.server_routes.map(route => `{
|
||||||
// ${route.file}
|
// ${route.file}
|
||||||
pattern: ${route.pattern},
|
pattern: ${route.pattern},
|
||||||
handlers: __${route.name},
|
handlers: ${route.name},
|
||||||
params: ${route.params.length > 0
|
params: ${route.params.length > 0
|
||||||
? `match => ({ ${route.params.map((param, i) => `${param}: d(match[${i + 1}])`).join(', ')} })`
|
? `match => ({ ${route.params.map((param, i) => `${param}: match[${i + 1}]`).join(', ')} })`
|
||||||
: `() => ({})`}
|
: `() => ({})`}
|
||||||
}`).join(',\n\n\t\t\t\t')}
|
}`).join(',\n\n\t\t\t\t')}
|
||||||
],
|
],
|
||||||
|
|
||||||
pages: [
|
pages: [
|
||||||
${manifest_data.pages.map(page => `{
|
${routes.pages.map(page => `{
|
||||||
// ${page.parts[page.parts.length - 1].component.file}
|
// ${page.parts[page.parts.length - 1].component.file}
|
||||||
pattern: ${page.pattern},
|
pattern: ${page.pattern},
|
||||||
parts: [
|
parts: [
|
||||||
@@ -186,12 +156,12 @@ function generate_server(
|
|||||||
|
|
||||||
const props = [
|
const props = [
|
||||||
`name: "${part.component.name}"`,
|
`name: "${part.component.name}"`,
|
||||||
`file: ${stringify(part.component.file)}`,
|
`file: "${part.component.file}"`,
|
||||||
`component: __${part.component.name}`
|
`component: ${part.component.name}`
|
||||||
];
|
];
|
||||||
|
|
||||||
if (part.params.length > 0) {
|
if (part.params.length > 0) {
|
||||||
const params = part.params.map((param, i) => `${param}: d(match[${i + 1}])`);
|
const params = part.params.map((param, i) => `${param}: match[${i + 1}]`);
|
||||||
props.push(`params: match => ({ ${params.join(', ')} })`);
|
props.push(`params: match => ({ ${params.join(', ')} })`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,16 +174,12 @@ function generate_server(
|
|||||||
root,
|
root,
|
||||||
|
|
||||||
error
|
error
|
||||||
};`.replace(/^\t\t/gm, '').trim();
|
};
|
||||||
|
|
||||||
const build_dir = path.relative(process.cwd(), locations.dest());
|
// this is included for legacy reasons
|
||||||
const src_dir = path.relative(process.cwd(), locations.src());
|
export const routes = {};`.replace(/^\t\t/gm, '').trim();
|
||||||
|
|
||||||
return `// This file is generated by Sapper — do not edit it!\n` + template
|
return code;
|
||||||
.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) {
|
function get_file(path_to_routes: string, component: PageComponent) {
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { locations } from '../config';
|
import { locations } from '../config';
|
||||||
import { Page, PageComponent, ServerRoute, ManifestData } from '../interfaces';
|
import { Page, PageComponent, ServerRoute } from '../interfaces';
|
||||||
import { posixify, reserved_words } from './utils';
|
import { posixify } from './utils';
|
||||||
|
|
||||||
export default function create_manifest_data(cwd = locations.routes()): ManifestData {
|
const default_layout_file = posixify(path.resolve(
|
||||||
// TODO remove in a future version
|
__dirname,
|
||||||
if (!fs.existsSync(cwd)) {
|
'../components/default-layout.html'
|
||||||
throw new Error(`As of Sapper 0.21, the routes/ directory should become src/routes/`);
|
));
|
||||||
}
|
|
||||||
|
|
||||||
|
export default function create_routes(cwd = locations.routes()) {
|
||||||
const components: PageComponent[] = [];
|
const components: PageComponent[] = [];
|
||||||
const pages: Page[] = [];
|
const pages: Page[] = [];
|
||||||
const server_routes: ServerRoute[] = [];
|
const server_routes: ServerRoute[] = [];
|
||||||
@@ -35,16 +35,13 @@ export default function create_manifest_data(cwd = locations.routes()): Manifest
|
|||||||
const file = path.relative(cwd, resolved);
|
const file = path.relative(cwd, resolved);
|
||||||
const is_dir = fs.statSync(resolved).isDirectory();
|
const is_dir = fs.statSync(resolved).isDirectory();
|
||||||
|
|
||||||
const ext = path.extname(basename);
|
|
||||||
if (!is_dir && !/^\.[a-z]+$/i.test(ext)) return null; // filter out tmp files etc
|
|
||||||
|
|
||||||
const segment = is_dir
|
const segment = is_dir
|
||||||
? basename
|
? basename
|
||||||
: basename.slice(0, -path.extname(basename).length);
|
: basename.slice(0, -path.extname(basename).length);
|
||||||
|
|
||||||
const parts = get_parts(segment);
|
const parts = get_parts(segment);
|
||||||
const is_index = is_dir ? false : basename.startsWith('index.');
|
const is_index = is_dir ? false : basename.startsWith('index.');
|
||||||
const is_page = ext === '.html';
|
const is_page = path.extname(basename) === '.html';
|
||||||
|
|
||||||
parts.forEach(part => {
|
parts.forEach(part => {
|
||||||
if (/\]\[/.test(part.content)) {
|
if (/\]\[/.test(part.content)) {
|
||||||
@@ -65,7 +62,6 @@ export default function create_manifest_data(cwd = locations.routes()): Manifest
|
|||||||
is_page
|
is_page
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter(Boolean)
|
|
||||||
.sort(comparator);
|
.sort(comparator);
|
||||||
|
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
@@ -137,12 +133,12 @@ export default function create_manifest_data(cwd = locations.routes()): Manifest
|
|||||||
components.push(component);
|
components.push(component);
|
||||||
if (item.basename === 'index.html') {
|
if (item.basename === 'index.html') {
|
||||||
pages.push({
|
pages.push({
|
||||||
pattern: get_pattern(parent_segments, true),
|
pattern: get_pattern(parent_segments),
|
||||||
parts
|
parts
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
pages.push({
|
pages.push({
|
||||||
pattern: get_pattern(segments, true),
|
pattern: get_pattern(segments),
|
||||||
parts
|
parts
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -151,7 +147,7 @@ export default function create_manifest_data(cwd = locations.routes()): Manifest
|
|||||||
else {
|
else {
|
||||||
server_routes.push({
|
server_routes.push({
|
||||||
name: `route_${get_slug(item.file)}`,
|
name: `route_${get_slug(item.file)}`,
|
||||||
pattern: get_pattern(segments, false),
|
pattern: get_pattern(segments),
|
||||||
file: item.file,
|
file: item.file,
|
||||||
params: params
|
params: params
|
||||||
});
|
});
|
||||||
@@ -274,7 +270,7 @@ function get_parts(part: string): Part[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function get_slug(file: string) {
|
function get_slug(file: string) {
|
||||||
let name = file
|
return file
|
||||||
.replace(/[\\\/]index/, '')
|
.replace(/[\\\/]index/, '')
|
||||||
.replace(/_default([\/\\index])?\.html$/, 'index')
|
.replace(/_default([\/\\index])?\.html$/, 'index')
|
||||||
.replace(/[\/\\]/g, '_')
|
.replace(/[\/\\]/g, '_')
|
||||||
@@ -283,12 +279,9 @@ function get_slug(file: string) {
|
|||||||
.replace(/[^a-zA-Z0-9_$]/g, c => {
|
.replace(/[^a-zA-Z0-9_$]/g, c => {
|
||||||
return c === '.' ? '_' : `$${c.charCodeAt(0)}`
|
return c === '.' ? '_' : `$${c.charCodeAt(0)}`
|
||||||
});
|
});
|
||||||
|
|
||||||
if (reserved_words.has(name)) name += '_';
|
|
||||||
return name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_pattern(segments: Part[][], add_trailing_slash: boolean) {
|
function get_pattern(segments: Part[][]) {
|
||||||
return new RegExp(
|
return new RegExp(
|
||||||
`^` +
|
`^` +
|
||||||
segments.map(segment => {
|
segments.map(segment => {
|
||||||
@@ -302,6 +295,6 @@ function get_pattern(segments: Part[][], add_trailing_slash: boolean) {
|
|||||||
.replace(/%5D/g, ']');
|
.replace(/%5D/g, ']');
|
||||||
}).join('');
|
}).join('');
|
||||||
}).join('') +
|
}).join('') +
|
||||||
(add_trailing_slash ? '\\\/?$' : '$')
|
'\\\/?$'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import * as fs from 'fs';
|
|
||||||
import { locations } from '../config';
|
|
||||||
|
|
||||||
export default function read_template(dir = locations.src()) {
|
|
||||||
try {
|
|
||||||
return fs.readFileSync(`${dir}/template.html`, 'utf-8');
|
|
||||||
} catch (err) {
|
|
||||||
if (fs.existsSync(`app/template.html`)) {
|
|
||||||
throw new Error(`As of Sapper 0.21, the default folder structure has been changed:
|
|
||||||
app/ --> src/
|
|
||||||
routes/ --> src/routes/
|
|
||||||
assets/ --> static/`);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import * as fs from 'fs';
|
import * as sander from 'sander';
|
||||||
|
|
||||||
const previous_contents = new Map();
|
const previous_contents = new Map();
|
||||||
|
|
||||||
export function write_if_changed(file: string, code: string) {
|
export function write_if_changed(file: string, code: string) {
|
||||||
if (code !== previous_contents.get(file)) {
|
if (code !== previous_contents.get(file)) {
|
||||||
previous_contents.set(file, code);
|
previous_contents.set(file, code);
|
||||||
fs.writeFileSync(file, code);
|
sander.writeFileSync(file, code);
|
||||||
fudge_mtime(file);
|
fudge_mtime(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,68 +14,12 @@ export function posixify(file: string) {
|
|||||||
return file.replace(/[/\\]/g, '/');
|
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) {
|
export function fudge_mtime(file: string) {
|
||||||
// need to fudge the mtime so that webpack doesn't go doolally
|
// need to fudge the mtime so that webpack doesn't go doolally
|
||||||
const { atime, mtime } = fs.statSync(file);
|
const { atime, mtime } = sander.statSync(file);
|
||||||
fs.utimesSync(
|
sander.utimesSync(
|
||||||
file,
|
file,
|
||||||
new Date(atime.getTime() - 999999),
|
new Date(atime.getTime() - 999999),
|
||||||
new Date(mtime.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',
|
|
||||||
]);
|
|
||||||
@@ -40,18 +40,3 @@ export type ServerRoute = {
|
|||||||
file: string;
|
file: string;
|
||||||
params: string[];
|
params: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Dirs = {
|
|
||||||
dest: string,
|
|
||||||
src: string,
|
|
||||||
routes: string,
|
|
||||||
webpack: string,
|
|
||||||
rollup: string
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ManifestData = {
|
|
||||||
root: PageComponent;
|
|
||||||
components: PageComponent[];
|
|
||||||
pages: Page[];
|
|
||||||
server_routes: ServerRoute[];
|
|
||||||
};
|
|
||||||
642
src/middleware.ts
Normal file
@@ -0,0 +1,642 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { URL } from 'url';
|
||||||
|
import { ClientRequest, ServerResponse } from 'http';
|
||||||
|
import cookie from 'cookie';
|
||||||
|
import devalue from 'devalue';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
import { lookup } from './middleware/mime';
|
||||||
|
import { locations, dev } from './config';
|
||||||
|
import sourceMapSupport from 'source-map-support';
|
||||||
|
import prettyBytes from 'pretty-bytes';
|
||||||
|
import { wrap_data } from './middleware/wrap_data';
|
||||||
|
import { list_unused_properties } from './middleware/list_unused_properties';
|
||||||
|
|
||||||
|
sourceMapSupport.install();
|
||||||
|
|
||||||
|
type ServerRoute = {
|
||||||
|
pattern: RegExp;
|
||||||
|
handlers: Record<string, Handler>;
|
||||||
|
params: (match: RegExpMatchArray) => Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Page = {
|
||||||
|
pattern: RegExp;
|
||||||
|
parts: Array<{
|
||||||
|
name: string;
|
||||||
|
component: Component;
|
||||||
|
params?: (match: RegExpMatchArray) => Record<string, string>;
|
||||||
|
}>
|
||||||
|
};
|
||||||
|
|
||||||
|
type Manifest = {
|
||||||
|
server_routes: ServerRoute[];
|
||||||
|
pages: Page[];
|
||||||
|
root: Component;
|
||||||
|
error: Component;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Handler = (req: Req, res: ServerResponse, next: () => void) => void;
|
||||||
|
|
||||||
|
type Store = {
|
||||||
|
get: () => any
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
path: string;
|
||||||
|
query: Record<string, string>;
|
||||||
|
params: Record<string, string>;
|
||||||
|
error?: { message: string };
|
||||||
|
status?: number;
|
||||||
|
child: {
|
||||||
|
segment: string;
|
||||||
|
component: Component;
|
||||||
|
props: Props;
|
||||||
|
};
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Req extends ClientRequest {
|
||||||
|
url: string;
|
||||||
|
baseUrl: string;
|
||||||
|
originalUrl: string;
|
||||||
|
method: string;
|
||||||
|
path: string;
|
||||||
|
params: Record<string, string>;
|
||||||
|
query: Record<string, string>;
|
||||||
|
headers: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Component {
|
||||||
|
render: (data: any, opts: { store: Store }) => {
|
||||||
|
head: string;
|
||||||
|
css: { code: string, map: any };
|
||||||
|
html: string
|
||||||
|
},
|
||||||
|
preload: (data: any) => any | Promise<any>
|
||||||
|
}
|
||||||
|
|
||||||
|
const IGNORE = '__SAPPER__IGNORE__';
|
||||||
|
function toIgnore(uri: string, val: any) {
|
||||||
|
if (Array.isArray(val)) return val.some(x => toIgnore(uri, x));
|
||||||
|
if (val instanceof RegExp) return val.test(uri);
|
||||||
|
if (typeof val === 'function') return val(uri);
|
||||||
|
return uri.startsWith(val.charCodeAt(0) === 47 ? val : `/${val}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function middleware(opts: {
|
||||||
|
manifest: Manifest,
|
||||||
|
store: (req: Req) => Store,
|
||||||
|
ignore?: any,
|
||||||
|
routes?: any // legacy
|
||||||
|
}) {
|
||||||
|
if (opts.routes) {
|
||||||
|
throw new Error(`As of Sapper 0.15, opts.routes should be opts.manifest`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = locations.dest();
|
||||||
|
|
||||||
|
const { manifest, store, ignore } = opts;
|
||||||
|
|
||||||
|
let emitted_basepath = false;
|
||||||
|
|
||||||
|
const middleware = compose_handlers([
|
||||||
|
ignore && ((req: Req, res: ServerResponse, next: () => void) => {
|
||||||
|
req[IGNORE] = toIgnore(req.path, ignore);
|
||||||
|
next();
|
||||||
|
}),
|
||||||
|
|
||||||
|
(req: Req, res: ServerResponse, next: () => void) => {
|
||||||
|
if (req[IGNORE]) return next();
|
||||||
|
|
||||||
|
if (req.baseUrl === undefined) {
|
||||||
|
let { originalUrl } = req;
|
||||||
|
if (req.url === '/' && originalUrl[originalUrl.length - 1] !== '/') {
|
||||||
|
originalUrl += '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
req.baseUrl = originalUrl
|
||||||
|
? originalUrl.slice(0, -req.url.length)
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!emitted_basepath && process.send) {
|
||||||
|
process.send({
|
||||||
|
__sapper__: true,
|
||||||
|
event: 'basepath',
|
||||||
|
basepath: req.baseUrl
|
||||||
|
});
|
||||||
|
|
||||||
|
emitted_basepath = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.path === undefined) {
|
||||||
|
req.path = req.url.replace(/\?.*/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
|
||||||
|
fs.existsSync(path.join(output, 'index.html')) && serve({
|
||||||
|
pathname: '/index.html',
|
||||||
|
cache_control: 'max-age=600'
|
||||||
|
}),
|
||||||
|
|
||||||
|
fs.existsSync(path.join(output, 'service-worker.js')) && serve({
|
||||||
|
pathname: '/service-worker.js',
|
||||||
|
cache_control: 'max-age=600'
|
||||||
|
}),
|
||||||
|
|
||||||
|
fs.existsSync(path.join(output, 'service-worker.js.map')) && serve({
|
||||||
|
pathname: '/service-worker.js.map',
|
||||||
|
cache_control: 'max-age=600'
|
||||||
|
}),
|
||||||
|
|
||||||
|
serve({
|
||||||
|
prefix: '/client/',
|
||||||
|
cache_control: 'max-age=31536000'
|
||||||
|
}),
|
||||||
|
|
||||||
|
get_server_route_handler(manifest.server_routes),
|
||||||
|
get_page_handler(manifest, store)
|
||||||
|
].filter(Boolean));
|
||||||
|
|
||||||
|
return middleware;
|
||||||
|
}
|
||||||
|
|
||||||
|
function serve({ prefix, pathname, cache_control }: {
|
||||||
|
prefix?: string,
|
||||||
|
pathname?: string,
|
||||||
|
cache_control: string
|
||||||
|
}) {
|
||||||
|
const filter = pathname
|
||||||
|
? (req: Req) => req.path === pathname
|
||||||
|
: (req: Req) => req.path.startsWith(prefix);
|
||||||
|
|
||||||
|
const output = locations.dest();
|
||||||
|
|
||||||
|
const cache: Map<string, Buffer> = new Map();
|
||||||
|
|
||||||
|
const read = dev()
|
||||||
|
? (file: string) => fs.readFileSync(path.resolve(output, file))
|
||||||
|
: (file: string) => (cache.has(file) ? cache : cache.set(file, fs.readFileSync(path.resolve(output, file)))).get(file)
|
||||||
|
|
||||||
|
return (req: Req, res: ServerResponse, next: () => void) => {
|
||||||
|
if (req[IGNORE]) return next();
|
||||||
|
|
||||||
|
if (filter(req)) {
|
||||||
|
const type = lookup(req.path);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = read(req.path.slice(1));
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', type);
|
||||||
|
res.setHeader('Cache-Control', cache_control);
|
||||||
|
res.end(data);
|
||||||
|
} catch (err) {
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.end('not found');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_server_route_handler(routes: ServerRoute[]) {
|
||||||
|
function handle_route(route: ServerRoute, req: Req, res: ServerResponse, next: () => void) {
|
||||||
|
req.params = route.params(route.pattern.exec(req.path));
|
||||||
|
|
||||||
|
const method = req.method.toLowerCase();
|
||||||
|
// 'delete' cannot be exported from a module because it is a keyword,
|
||||||
|
// so check for 'del' instead
|
||||||
|
const method_export = method === 'delete' ? 'del' : method;
|
||||||
|
const handle_method = route.handlers[method_export];
|
||||||
|
if (handle_method) {
|
||||||
|
if (process.env.SAPPER_EXPORT) {
|
||||||
|
const { write, end, setHeader } = res;
|
||||||
|
const chunks: any[] = [];
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
|
||||||
|
// intercept data so that it can be exported
|
||||||
|
res.write = function(chunk: any) {
|
||||||
|
chunks.push(new Buffer(chunk));
|
||||||
|
write.apply(res, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
res.setHeader = function(name: string, value: string) {
|
||||||
|
headers[name.toLowerCase()] = value;
|
||||||
|
setHeader.apply(res, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
res.end = function(chunk?: any) {
|
||||||
|
if (chunk) chunks.push(new Buffer(chunk));
|
||||||
|
end.apply(res, arguments);
|
||||||
|
|
||||||
|
process.send({
|
||||||
|
__sapper__: true,
|
||||||
|
event: 'file',
|
||||||
|
url: req.url,
|
||||||
|
method: req.method,
|
||||||
|
status: res.statusCode,
|
||||||
|
type: headers['content-type'],
|
||||||
|
body: Buffer.concat(chunks).toString()
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const handle_next = (err?: Error) => {
|
||||||
|
if (err) {
|
||||||
|
res.statusCode = 500;
|
||||||
|
res.end(err.message);
|
||||||
|
} else {
|
||||||
|
process.nextTick(next);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
handle_method(req, res, handle_next);
|
||||||
|
} catch (err) {
|
||||||
|
handle_next(err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// no matching handler for method
|
||||||
|
process.nextTick(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return function find_route(req: Req, res: ServerResponse, next: () => void) {
|
||||||
|
if (req[IGNORE]) return next();
|
||||||
|
|
||||||
|
for (const route of routes) {
|
||||||
|
if (route.pattern.test(req.path)) {
|
||||||
|
handle_route(route, req, res, next);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_page_handler(manifest: Manifest, store_getter: (req: Req) => Store) {
|
||||||
|
const output = locations.dest();
|
||||||
|
|
||||||
|
const get_chunks = dev()
|
||||||
|
? () => JSON.parse(fs.readFileSync(path.join(output, 'client_assets.json'), 'utf-8'))
|
||||||
|
: (assets => () => assets)(JSON.parse(fs.readFileSync(path.join(output, 'client_assets.json'), 'utf-8')));
|
||||||
|
|
||||||
|
const template = dev()
|
||||||
|
? () => fs.readFileSync(`${locations.app()}/template.html`, 'utf-8')
|
||||||
|
: (str => () => str)(fs.readFileSync(`${locations.dest()}/template.html`, 'utf-8'));
|
||||||
|
|
||||||
|
const { server_routes, pages } = manifest;
|
||||||
|
const error_route = manifest.error;
|
||||||
|
|
||||||
|
const should_wrap_data = dev() || process.env.SAPPER_EXPORT;
|
||||||
|
|
||||||
|
function handle_error(req: Req, res: ServerResponse, statusCode: number, error: Error | string) {
|
||||||
|
handle_page({
|
||||||
|
pattern: null,
|
||||||
|
parts: [
|
||||||
|
{ name: null, component: error_route }
|
||||||
|
]
|
||||||
|
}, req, res, statusCode, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_page(page: Page, req: Req, res: ServerResponse, status = 200, error: Error | string = null) {
|
||||||
|
const get_params = page.parts[page.parts.length - 1].params || (() => ({}));
|
||||||
|
const match = error ? null : page.pattern.exec(req.path);
|
||||||
|
|
||||||
|
const chunks: Record<string, string | string[]> = get_chunks();
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'text/html');
|
||||||
|
|
||||||
|
// preload main.js and current route
|
||||||
|
// TODO detect other stuff we can preload? images, CSS, fonts?
|
||||||
|
let preloaded_chunks = Array.isArray(chunks.main) ? chunks.main : [chunks.main];
|
||||||
|
if (!error) {
|
||||||
|
page.parts.forEach(part => {
|
||||||
|
if (!part) return;
|
||||||
|
|
||||||
|
// using concat because it could be a string or an array. thanks webpack!
|
||||||
|
preloaded_chunks = preloaded_chunks.concat(chunks[part.name]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const link = preloaded_chunks
|
||||||
|
.filter(file => !file.match(/\.map$/))
|
||||||
|
.map(file => `<${req.baseUrl}/client/${file}>;rel="preload";as="script"`)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
res.setHeader('Link', link);
|
||||||
|
|
||||||
|
const store = store_getter ? store_getter(req) : null;
|
||||||
|
|
||||||
|
let redirect: { statusCode: number, location: string };
|
||||||
|
let preload_error: { statusCode: number, message: Error | string };
|
||||||
|
|
||||||
|
const preload_context = {
|
||||||
|
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) => {
|
||||||
|
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 + '/' :''}`);
|
||||||
|
|
||||||
|
if (opts) {
|
||||||
|
opts = Object.assign({}, opts);
|
||||||
|
|
||||||
|
const include_cookies = (
|
||||||
|
opts.credentials === 'include' ||
|
||||||
|
opts.credentials === 'same-origin' && parsed.origin === `http://127.0.0.1:${process.env.PORT}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (include_cookies) {
|
||||||
|
const cookies: Record<string, string> = {};
|
||||||
|
if (!opts.headers) 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(', ');
|
||||||
|
|
||||||
|
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: {}
|
||||||
|
})
|
||||||
|
: {};
|
||||||
|
|
||||||
|
Promise.all([root_preloaded].concat(page.parts.map(part => {
|
||||||
|
if (!part) return null;
|
||||||
|
|
||||||
|
return part.component.preload
|
||||||
|
? part.component.preload.call(preload_context, {
|
||||||
|
path: req.path,
|
||||||
|
query: req.query,
|
||||||
|
params: part.params ? part.params(match) : {}
|
||||||
|
})
|
||||||
|
: {};
|
||||||
|
}))).catch(err => {
|
||||||
|
preload_error = { statusCode: 500, message: err };
|
||||||
|
return []; // appease TypeScript
|
||||||
|
}).then(preloaded => {
|
||||||
|
if (redirect) {
|
||||||
|
const location = `${req.baseUrl}/${redirect.location}`;
|
||||||
|
|
||||||
|
res.statusCode = redirect.statusCode;
|
||||||
|
res.setHeader('Location', location);
|
||||||
|
res.end();
|
||||||
|
|
||||||
|
if (process.send) {
|
||||||
|
process.send({
|
||||||
|
__sapper__: true,
|
||||||
|
event: 'file',
|
||||||
|
url: req.url,
|
||||||
|
method: req.method,
|
||||||
|
status: redirect.statusCode,
|
||||||
|
type: 'text/html',
|
||||||
|
body: `<script>window.location.href = "${location}"</script>`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preload_error) {
|
||||||
|
handle_error(req, res, preload_error.statusCode, preload_error.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const segments = req.path.split('/').filter(Boolean);
|
||||||
|
|
||||||
|
const props: Props = {
|
||||||
|
path: req.path,
|
||||||
|
query: req.query,
|
||||||
|
params: {},
|
||||||
|
child: null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
props.error = error instanceof Error ? error : { message: error };
|
||||||
|
props.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = Object.assign({}, props, preloaded[0], {
|
||||||
|
params: {},
|
||||||
|
child: {
|
||||||
|
segment: segments[0]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// in dev and export modes, we wrap data in proxies to see
|
||||||
|
// how much of it is used in the initial render
|
||||||
|
const wrapped = should_wrap_data && wrap_data(preloaded);
|
||||||
|
|
||||||
|
// this is an easy way to 'reify' top-level values
|
||||||
|
const _preloaded = should_wrap_data
|
||||||
|
? wrapped.data.map((x: any) => x)
|
||||||
|
: preloaded;
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
let scripts = []
|
||||||
|
.concat(chunks.main) // chunks main might be an array. it might not! thanks, webpack
|
||||||
|
.filter(file => !file.match(/\.map$/))
|
||||||
|
.map(file => `<script src='${req.baseUrl}/client/${file}'></script>`)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
const unwrapped = should_wrap_data && wrapped.unwrap();
|
||||||
|
|
||||||
|
const preloaded_serialized = preloaded.map(try_serialize);
|
||||||
|
|
||||||
|
if (should_wrap_data && process.send) {
|
||||||
|
const discrepancies = [];
|
||||||
|
|
||||||
|
unwrapped.forEach((clone, i) => {
|
||||||
|
const loaded = preloaded_serialized[i];
|
||||||
|
if (!loaded) return;
|
||||||
|
|
||||||
|
const rendered = try_serialize(clone);
|
||||||
|
|
||||||
|
if (rendered !== loaded) {
|
||||||
|
const part = page.parts[i - 1];
|
||||||
|
const file = part ? part.file : '_layout.html';
|
||||||
|
|
||||||
|
discrepancies.push({
|
||||||
|
file,
|
||||||
|
preloaded: loaded.length,
|
||||||
|
rendered: rendered.length,
|
||||||
|
props: list_unused_properties(preloaded[i], clone)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (discrepancies.length) {
|
||||||
|
process.send({
|
||||||
|
__sapper__: true,
|
||||||
|
event: 'unused_data',
|
||||||
|
url: req.url,
|
||||||
|
discrepancies
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const serialized = {
|
||||||
|
preloaded: `[${preloaded_serialized.join(',')}]`,
|
||||||
|
store: store && try_serialize(store.get())
|
||||||
|
};
|
||||||
|
|
||||||
|
let inline_script = `__SAPPER__={${[
|
||||||
|
error && `error:1`,
|
||||||
|
`baseUrl:"${req.baseUrl}"`,
|
||||||
|
serialized.preloaded && `preloaded:${serialized.preloaded}`,
|
||||||
|
serialized.store && `store:${serialized.store}`
|
||||||
|
].filter(Boolean).join(',')}};`;
|
||||||
|
|
||||||
|
const has_service_worker = fs.existsSync(path.join(locations.dest(), 'service-worker.js'));
|
||||||
|
if (has_service_worker) {
|
||||||
|
inline_script += `if ('serviceWorker' in navigator) navigator.serviceWorker.register('${req.baseUrl}/service-worker.js');`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = template()
|
||||||
|
.replace('%sapper.base%', () => `<base href="${req.baseUrl}/">`)
|
||||||
|
.replace('%sapper.scripts%', () => `<script>${inline_script}</script>${scripts}`)
|
||||||
|
.replace('%sapper.html%', () => html)
|
||||||
|
.replace('%sapper.head%', () => `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`)
|
||||||
|
.replace('%sapper.styles%', () => (css && css.code ? `<style>${css.code}</style>` : ''));
|
||||||
|
|
||||||
|
res.statusCode = status;
|
||||||
|
res.end(body);
|
||||||
|
|
||||||
|
if (process.send) {
|
||||||
|
process.send({
|
||||||
|
__sapper__: true,
|
||||||
|
event: 'preload',
|
||||||
|
url: req.url,
|
||||||
|
size: serialized.preloaded.length
|
||||||
|
});
|
||||||
|
|
||||||
|
process.send({
|
||||||
|
__sapper__: true,
|
||||||
|
event: 'file',
|
||||||
|
url: req.url,
|
||||||
|
method: req.method,
|
||||||
|
status,
|
||||||
|
type: 'text/html',
|
||||||
|
body
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
if (error) {
|
||||||
|
// we encountered an error while rendering the error page — oops
|
||||||
|
res.statusCode = 500;
|
||||||
|
res.end(`<pre>${escape_html(err.message)}</pre>`);
|
||||||
|
} else {
|
||||||
|
handle_error(req, res, 500, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return function find_route(req: Req, res: ServerResponse, 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_error(req, res, 404, 'Not found');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function compose_handlers(handlers: Handler[]) {
|
||||||
|
return (req: Req, res: ServerResponse, next: () => void) => {
|
||||||
|
let i = 0;
|
||||||
|
function go() {
|
||||||
|
const handler = handlers[i];
|
||||||
|
|
||||||
|
if (handler) {
|
||||||
|
handler(req, res, () => {
|
||||||
|
i += 1;
|
||||||
|
go();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function try_serialize(data: any) {
|
||||||
|
try {
|
||||||
|
return devalue(data);
|
||||||
|
} catch (err) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function escape_html(html: string) {
|
||||||
|
const chars: Record<string, string> = {
|
||||||
|
'"' : 'quot',
|
||||||
|
"'": '#39',
|
||||||
|
'&': 'amp',
|
||||||
|
'<' : 'lt',
|
||||||
|
'>' : 'gt'
|
||||||
|
};
|
||||||
|
|
||||||
|
return html.replace(/["'&<>]/g, c => `&${chars[c]};`);
|
||||||
|
}
|
||||||
34
src/middleware/list_unused_properties.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
export function list_unused_properties(all: any, used: any) {
|
||||||
|
const props: string[] = [];
|
||||||
|
|
||||||
|
const seen = new Set();
|
||||||
|
|
||||||
|
function walk(keypath: string, a: any, b: any) {
|
||||||
|
if (seen.has(a)) return;
|
||||||
|
seen.add(a);
|
||||||
|
|
||||||
|
if (!a || typeof a !== 'object') return;
|
||||||
|
|
||||||
|
const is_array = Array.isArray(a);
|
||||||
|
|
||||||
|
for (const key in a) {
|
||||||
|
const child_keypath = keypath
|
||||||
|
? is_array ? `${keypath}[${key}]` : `${keypath}.${key}`
|
||||||
|
: key;
|
||||||
|
|
||||||
|
if (hasProp.call(b, key)) {
|
||||||
|
const a_child = a[key];
|
||||||
|
const b_child = b[key];
|
||||||
|
|
||||||
|
walk(child_keypath, a_child, b_child);
|
||||||
|
} else {
|
||||||
|
props.push(child_keypath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
walk(null, all, used);
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasProp = Object.prototype.hasOwnProperty;
|
||||||
85
src/middleware/wrap_data.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
type Obj = Record<string, any>;
|
||||||
|
|
||||||
|
export function wrap_data(data: any) {
|
||||||
|
const proxies = new Map();
|
||||||
|
const clones = new Map();
|
||||||
|
|
||||||
|
const handler = {
|
||||||
|
get(target: any, property: string): any {
|
||||||
|
const value = target[property];
|
||||||
|
const intercepted = intercept(value);
|
||||||
|
|
||||||
|
const target_clone = clones.get(target);
|
||||||
|
const child_clone = clones.get(value);
|
||||||
|
|
||||||
|
if (target_clone && target.hasOwnProperty(property)) {
|
||||||
|
target_clone[property] = child_clone || value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return intercepted;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function get_or_create_proxy(obj: any) {
|
||||||
|
if (!proxies.has(obj)) {
|
||||||
|
proxies.set(obj, new Proxy(obj, handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxies.get(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
function intercept(obj: any) {
|
||||||
|
if (clones.has(obj)) return obj;
|
||||||
|
|
||||||
|
if (obj && typeof obj === 'object') {
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
clones.set(obj, []);
|
||||||
|
return get_or_create_proxy(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (isPlainObject(obj)) {
|
||||||
|
clones.set(obj, {});
|
||||||
|
return get_or_create_proxy(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clones.set(obj, obj);
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: intercept(data),
|
||||||
|
unwrap: () => {
|
||||||
|
return clones.get(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectProtoOwnPropertyNames = Object.getOwnPropertyNames(Object.prototype).sort().join('\0')
|
||||||
|
|
||||||
|
function isPlainObject(obj: any) {
|
||||||
|
const proto = Object.getPrototypeOf(obj);
|
||||||
|
|
||||||
|
if (
|
||||||
|
proto !== Object.prototype &&
|
||||||
|
proto !== null &&
|
||||||
|
Object.getOwnPropertyNames(proto).sort().join('\0') !== objectProtoOwnPropertyNames
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.getOwnPropertySymbols(obj).length > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function pick(obj: Obj, props: string[]) {
|
||||||
|
const picked: Obj = {};
|
||||||
|
props.forEach(prop => {
|
||||||
|
picked[prop] = obj[prop];
|
||||||
|
});
|
||||||
|
return picked;
|
||||||
|
}
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import { locations, dev } from './config';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
dev: dev(),
|
|
||||||
|
|
||||||
client: {
|
|
||||||
input: () => {
|
|
||||||
return `${locations.src()}/client.js`
|
|
||||||
},
|
|
||||||
|
|
||||||
output: () => {
|
|
||||||
let dir = `${locations.dest()}/client`;
|
|
||||||
if (process.env.SAPPER_LEGACY_BUILD) dir += `/legacy`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
dir,
|
|
||||||
entryFileNames: '[name].[hash].js',
|
|
||||||
chunkFileNames: '[name].[hash].js',
|
|
||||||
format: 'esm',
|
|
||||||
sourcemap: dev()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
server: {
|
|
||||||
input: () => {
|
|
||||||
return {
|
|
||||||
server: `${locations.src()}/server.js`
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
output: () => {
|
|
||||||
return {
|
|
||||||
dir: `${locations.dest()}/server`,
|
|
||||||
format: 'cjs',
|
|
||||||
sourcemap: dev()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
serviceworker: {
|
|
||||||
input: () => {
|
|
||||||
return `${locations.src()}/service-worker.js`;
|
|
||||||
},
|
|
||||||
|
|
||||||
output: () => {
|
|
||||||
return {
|
|
||||||
file: `${locations.dest()}/service-worker.js`,
|
|
||||||
format: 'iife'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
478
src/runtime/index.ts
Normal file
@@ -0,0 +1,478 @@
|
|||||||
|
import { detach, findAnchor, scroll_state, which } from './utils';
|
||||||
|
import { Component, ComponentConstructor, Params, Query, Redirect, Manifest, RouteData, ScrollPosition, Store, Target } from './interfaces';
|
||||||
|
|
||||||
|
const initial_data = typeof window !== 'undefined' && window.__SAPPER__;
|
||||||
|
|
||||||
|
export let root: Component;
|
||||||
|
let target: Node;
|
||||||
|
let store: Store;
|
||||||
|
let manifest: Manifest;
|
||||||
|
let segments: string[] = [];
|
||||||
|
|
||||||
|
type RootProps = {
|
||||||
|
path: string;
|
||||||
|
params: Record<string, string>;
|
||||||
|
query: Record<string, string>;
|
||||||
|
child: Child;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Child = {
|
||||||
|
segment?: string;
|
||||||
|
props?: any;
|
||||||
|
component?: Component;
|
||||||
|
};
|
||||||
|
|
||||||
|
const root_props: RootProps = {
|
||||||
|
path: null,
|
||||||
|
params: null,
|
||||||
|
query: null,
|
||||||
|
child: {
|
||||||
|
segment: null,
|
||||||
|
component: null,
|
||||||
|
props: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { root as component }; // legacy reasons — drop in a future version
|
||||||
|
|
||||||
|
const history = typeof window !== 'undefined' ? window.history : {
|
||||||
|
pushState: (state: any, title: string, href: string) => {},
|
||||||
|
replaceState: (state: any, title: string, href: string) => {},
|
||||||
|
scrollRestoration: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const scroll_history: Record<string, ScrollPosition> = {};
|
||||||
|
let uid = 1;
|
||||||
|
let cid: number;
|
||||||
|
|
||||||
|
if ('scrollRestoration' in history) {
|
||||||
|
history.scrollRestoration = 'manual';
|
||||||
|
}
|
||||||
|
|
||||||
|
function select_route(url: URL): Target {
|
||||||
|
if (url.origin !== window.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 (manifest.ignore.some(pattern => pattern.test(path))) return;
|
||||||
|
|
||||||
|
for (let i = 0; i < manifest.pages.length; i += 1) {
|
||||||
|
const page = manifest.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] = value || true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { url, path, page, match, query };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_token: {};
|
||||||
|
|
||||||
|
function render(data: any, nullable_depth: number, scroll: ScrollPosition, token: {}) {
|
||||||
|
if (current_token !== token) return;
|
||||||
|
|
||||||
|
if (root) {
|
||||||
|
// 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.set({ child: data.child });
|
||||||
|
|
||||||
|
// then render new stuff
|
||||||
|
level.component = component;
|
||||||
|
root.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 = new manifest.root({
|
||||||
|
target,
|
||||||
|
data,
|
||||||
|
store,
|
||||||
|
hydrate: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scroll) {
|
||||||
|
window.scrollTo(scroll.x, scroll.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(root_props, data);
|
||||||
|
ready = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function changed(a: Record<string, string | true>, b: Record<string, string | true>) {
|
||||||
|
return JSON.stringify(a) !== JSON.stringify(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
let root_preload: Promise<any>;
|
||||||
|
let root_data: any;
|
||||||
|
|
||||||
|
function prepare_page(target: Target): Promise<{
|
||||||
|
redirect?: Redirect;
|
||||||
|
data?: any;
|
||||||
|
nullable_depth?: number;
|
||||||
|
}> {
|
||||||
|
if (root) {
|
||||||
|
root.set({ preloading: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
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) => window.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 = manifest.root.preload
|
||||||
|
? initial_data.preloaded[0] || manifest.root.preload.call(preload_context, {
|
||||||
|
path,
|
||||||
|
query,
|
||||||
|
params: {}
|
||||||
|
})
|
||||||
|
: {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(page.parts.map(async (part, i) => {
|
||||||
|
if (i < changed_from) return null;
|
||||||
|
if (!part) return null;
|
||||||
|
|
||||||
|
const { default: Component } = await part.component();
|
||||||
|
const req = {
|
||||||
|
path,
|
||||||
|
query,
|
||||||
|
params: part.params ? part.params(target.match) : {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const preloaded = ready || !initial_data.preloaded[i + 1]
|
||||||
|
? Component.preload ? await Component.preload.call(preload_context, req) : {}
|
||||||
|
: initial_data.preloaded[i + 1];
|
||||||
|
|
||||||
|
return { Component, preloaded };
|
||||||
|
})).catch(err => {
|
||||||
|
error = { statusCode: 500, message: err };
|
||||||
|
return [];
|
||||||
|
}).then(async results => {
|
||||||
|
if (!root_data) root_data = await root_preload;
|
||||||
|
|
||||||
|
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: manifest.error,
|
||||||
|
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 };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async 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;
|
||||||
|
|
||||||
|
const loaded = prefetching && prefetching.href === target.url.href ?
|
||||||
|
prefetching.promise :
|
||||||
|
prepare_page(target);
|
||||||
|
|
||||||
|
prefetching = null;
|
||||||
|
|
||||||
|
const token = current_token = {};
|
||||||
|
const { redirect, data, nullable_depth } = await loaded;
|
||||||
|
|
||||||
|
if (redirect) {
|
||||||
|
await goto(redirect.location, { replaceState: true });
|
||||||
|
} else {
|
||||||
|
render(data, nullable_depth, scroll_history[id], token);
|
||||||
|
if (document.activeElement) document.activeElement.blur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_click(event: MouseEvent) {
|
||||||
|
// Adapted from https://github.com/visionmedia/page.js
|
||||||
|
// MIT license https://github.com/visionmedia/page.js#license
|
||||||
|
if (which(event) !== 1) return;
|
||||||
|
if (event.metaKey || event.ctrlKey || event.shiftKey) return;
|
||||||
|
if (event.defaultPrevented) return;
|
||||||
|
|
||||||
|
const a: HTMLAnchorElement | SVGAElement = <HTMLAnchorElement | SVGAElement>findAnchor(<Node>event.target);
|
||||||
|
if (!a) return;
|
||||||
|
|
||||||
|
// check if link is inside an svg
|
||||||
|
// in this case, both href and target are always inside an object
|
||||||
|
const svg = typeof a.href === 'object' && a.href.constructor.name === 'SVGAnimatedString';
|
||||||
|
const href = String(svg ? (<SVGAElement>a).href.baseVal : a.href);
|
||||||
|
|
||||||
|
if (href === window.location.href) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore if tag has
|
||||||
|
// 1. 'download' attribute
|
||||||
|
// 2. rel='external' attribute
|
||||||
|
if (a.hasAttribute('download') || a.getAttribute('rel') === 'external') return;
|
||||||
|
|
||||||
|
// Ignore if <a> has a target
|
||||||
|
if (svg ? (<SVGAElement>a).target.baseVal : a.target) return;
|
||||||
|
|
||||||
|
const url = new URL(href);
|
||||||
|
|
||||||
|
// Don't handle hash changes
|
||||||
|
if (url.pathname === window.location.pathname && url.search === window.location.search) return;
|
||||||
|
|
||||||
|
const target = select_route(url);
|
||||||
|
if (target) {
|
||||||
|
navigate(target, null);
|
||||||
|
event.preventDefault();
|
||||||
|
history.pushState({ id: cid }, '', url.href);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_popstate(event: PopStateEvent) {
|
||||||
|
scroll_history[cid] = scroll_state();
|
||||||
|
|
||||||
|
if (event.state) {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
const target = select_route(url);
|
||||||
|
if (target) {
|
||||||
|
navigate(target, event.state.id);
|
||||||
|
} else {
|
||||||
|
window.location.href = window.location.href;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// hashchange
|
||||||
|
cid = ++uid;
|
||||||
|
history.replaceState({ id: cid }, '', window.location.href);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let prefetching: {
|
||||||
|
href: string;
|
||||||
|
promise: Promise<{ redirect?: Redirect, data?: any, nullable_depth?: number }>;
|
||||||
|
} = null;
|
||||||
|
|
||||||
|
export function prefetch(href: string) {
|
||||||
|
const target: Target = select_route(new URL(href, document.baseURI));
|
||||||
|
|
||||||
|
if (target && (!prefetching || href !== prefetching.href)) {
|
||||||
|
prefetching = {
|
||||||
|
href,
|
||||||
|
promise: prepare_page(target)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mousemove_timeout: NodeJS.Timer;
|
||||||
|
|
||||||
|
function handle_mousemove(event: MouseEvent) {
|
||||||
|
clearTimeout(mousemove_timeout);
|
||||||
|
mousemove_timeout = setTimeout(() => {
|
||||||
|
trigger_prefetch(event);
|
||||||
|
}, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
function trigger_prefetch(event: MouseEvent | TouchEvent) {
|
||||||
|
const a: HTMLAnchorElement = <HTMLAnchorElement>findAnchor(<Node>event.target);
|
||||||
|
if (!a || a.rel !== 'prefetch') return;
|
||||||
|
|
||||||
|
prefetch(a.href);
|
||||||
|
}
|
||||||
|
|
||||||
|
let inited: boolean;
|
||||||
|
let ready = false;
|
||||||
|
|
||||||
|
export function init(opts: {
|
||||||
|
App: ComponentConstructor,
|
||||||
|
target: Node,
|
||||||
|
manifest: Manifest,
|
||||||
|
store?: (data: any) => Store,
|
||||||
|
routes?: any // legacy
|
||||||
|
}) {
|
||||||
|
if (opts instanceof HTMLElement) {
|
||||||
|
throw new Error(`The signature of init(...) has changed — see https://sapper.svelte.technology/guide#0-11-to-0-12 for more information`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.routes) {
|
||||||
|
throw new Error(`As of Sapper 0.15, opts.routes should be opts.manifest`);
|
||||||
|
}
|
||||||
|
|
||||||
|
target = opts.target;
|
||||||
|
manifest = opts.manifest;
|
||||||
|
|
||||||
|
if (opts && opts.store) {
|
||||||
|
store = opts.store(initial_data.store);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inited) { // this check makes HMR possible
|
||||||
|
window.addEventListener('click', handle_click);
|
||||||
|
window.addEventListener('popstate', handle_popstate);
|
||||||
|
|
||||||
|
// prefetch
|
||||||
|
window.addEventListener('touchstart', trigger_prefetch);
|
||||||
|
window.addEventListener('mousemove', handle_mousemove);
|
||||||
|
|
||||||
|
inited = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve().then(() => {
|
||||||
|
const { hash, href } = window.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(window.location.href));
|
||||||
|
if (target) return navigate(target, uid);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function goto(href: string, opts = { replaceState: false }) {
|
||||||
|
const target = select_route(new URL(href, document.baseURI));
|
||||||
|
let promise;
|
||||||
|
|
||||||
|
if (target) {
|
||||||
|
promise = navigate(target, null);
|
||||||
|
if (history) history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
|
||||||
|
} else {
|
||||||
|
window.location.href = href;
|
||||||
|
promise = new Promise(f => {}); // never resolves
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prefetchRoutes(pathnames: string[]) {
|
||||||
|
if (!manifest) throw new Error(`You must call init() first`);
|
||||||
|
|
||||||
|
return manifest.pages
|
||||||
|
.filter(route => {
|
||||||
|
if (!pathnames) return true;
|
||||||
|
return pathnames.some(pathname => route.pattern.test(pathname));
|
||||||
|
})
|
||||||
|
.reduce((promise: Promise<any>, route) => {
|
||||||
|
return promise.then(route.load);
|
||||||
|
}, Promise.resolve());
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove this in 0.9
|
||||||
|
export { prefetchRoutes as preloadRoutes };
|
||||||
@@ -1,20 +1,10 @@
|
|||||||
|
import { Store } from '../interfaces';
|
||||||
|
|
||||||
|
export { Store };
|
||||||
export type Params = Record<string, string>;
|
export type Params = Record<string, string>;
|
||||||
export type Query = Record<string, string | true>;
|
export type Query = Record<string, string | true>;
|
||||||
export type RouteData = { params: Params, query: Query, path: string };
|
export type RouteData = { params: Params, query: Query, path: string };
|
||||||
|
|
||||||
type Child = {
|
|
||||||
segment?: string;
|
|
||||||
props?: any;
|
|
||||||
component?: Component;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RootProps = {
|
|
||||||
path: string;
|
|
||||||
params: Record<string, string>;
|
|
||||||
query: Record<string, string>;
|
|
||||||
child: Child;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface ComponentConstructor {
|
export interface ComponentConstructor {
|
||||||
new (options: { target: Node, data: any, store: Store, hydrate: boolean }): Component;
|
new (options: { target: Node, data: any, store: Store, hydrate: boolean }): Component;
|
||||||
preload: (props: { params: Params, query: Query }) => Promise<any>;
|
preload: (props: { params: Params, query: Query }) => Promise<any>;
|
||||||
@@ -25,15 +15,10 @@ export interface Component {
|
|||||||
destroy: () => void;
|
destroy: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComponentLoader = {
|
|
||||||
js: () => Promise<{ default: ComponentConstructor }>,
|
|
||||||
css: string[]
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Page = {
|
export type Page = {
|
||||||
pattern: RegExp;
|
pattern: RegExp;
|
||||||
parts: Array<{
|
parts: Array<{
|
||||||
i: number;
|
component: () => Promise<{ default: ComponentConstructor }>;
|
||||||
params?: (match: RegExpExecArray) => Record<string, string>;
|
params?: (match: RegExpExecArray) => Record<string, string>;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
@@ -62,7 +47,3 @@ export type Redirect = {
|
|||||||
statusCode: number;
|
statusCode: number;
|
||||||
location: string;
|
location: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Store = {
|
|
||||||
get: () => any;
|
|
||||||
}
|
|
||||||
19
src/runtime/utils.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export function detach(node: Node) {
|
||||||
|
node.parentNode.removeChild(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findAnchor(node: Node) {
|
||||||
|
while (node && node.nodeName.toUpperCase() !== 'A') node = node.parentNode; // SVG <a> elements have a lowercase name
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function which(event: MouseEvent) {
|
||||||
|
return event.which === null ? event.button : event.which;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scroll_state() {
|
||||||
|
return {
|
||||||
|
x: window.scrollX,
|
||||||
|
y: window.scrollY
|
||||||
|
};
|
||||||
|
}
|
||||||
10
src/utils.ts
@@ -1,10 +0,0 @@
|
|||||||
export function left_pad(str: string, len: number) {
|
|
||||||
while (str.length < len) str = ` ${str}`;
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function repeat(str: string, i: number) {
|
|
||||||
let result = '';
|
|
||||||
while (i--) result += str;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ export default {
|
|||||||
client: {
|
client: {
|
||||||
entry: () => {
|
entry: () => {
|
||||||
return {
|
return {
|
||||||
main: `${locations.src()}/client`
|
main: `${locations.app()}/client`
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -23,13 +23,13 @@ export default {
|
|||||||
server: {
|
server: {
|
||||||
entry: () => {
|
entry: () => {
|
||||||
return {
|
return {
|
||||||
server: `${locations.src()}/server`
|
server: `${locations.app()}/server`
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
output: () => {
|
output: () => {
|
||||||
return {
|
return {
|
||||||
path: `${locations.dest()}/server`,
|
path: locations.dest(),
|
||||||
filename: '[name].js',
|
filename: '[name].js',
|
||||||
chunkFilename: '[hash]/[name].[id].js',
|
chunkFilename: '[hash]/[name].[id].js',
|
||||||
libraryTarget: 'commonjs2'
|
libraryTarget: 'commonjs2'
|
||||||
@@ -40,7 +40,7 @@ export default {
|
|||||||
serviceworker: {
|
serviceworker: {
|
||||||
entry: () => {
|
entry: () => {
|
||||||
return {
|
return {
|
||||||
'service-worker': `${locations.src()}/service-worker`
|
'service-worker': `${locations.app()}/service-worker`
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,374 +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, noscroll = false): 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) {
|
|
||||||
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 = `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,13 +0,0 @@
|
|||||||
import { history, select_route, navigate, cid } from '../app';
|
|
||||||
|
|
||||||
export default function goto(href: string, opts = { replaceState: false }) {
|
|
||||||
const target = select_route(new URL(href, document.baseURI));
|
|
||||||
|
|
||||||
if (target) {
|
|
||||||
history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
|
|
||||||
return navigate(target, null).then(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
location.href = href;
|
|
||||||
return new Promise(f => {}); // never resolves
|
|
||||||
}
|
|
||||||
@@ -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,12 +0,0 @@
|
|||||||
import { components, pages, 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));
|
|
||||||
})
|
|
||||||
.reduce((promise: Promise<any>, route) => promise.then(() => {
|
|
||||||
return Promise.all(route.parts.map(part => part && load_component(components[part.i])));
|
|
||||||
}), Promise.resolve());
|
|
||||||
}
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
import {
|
|
||||||
cid,
|
|
||||||
history,
|
|
||||||
initial_data,
|
|
||||||
navigate,
|
|
||||||
scroll_history,
|
|
||||||
scroll_state,
|
|
||||||
select_route,
|
|
||||||
set_store,
|
|
||||||
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
|
|
||||||
}) {
|
|
||||||
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);
|
|
||||||
|
|
||||||
// prefetch
|
|
||||||
addEventListener('touchstart', trigger_prefetch);
|
|
||||||
addEventListener('mousemove', handle_mousemove);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let mousemove_timeout: NodeJS.Timer;
|
|
||||||
|
|
||||||
function handle_mousemove(event: MouseEvent) {
|
|
||||||
clearTimeout(mousemove_timeout);
|
|
||||||
mousemove_timeout = setTimeout(() => {
|
|
||||||
trigger_prefetch(event);
|
|
||||||
}, 20);
|
|
||||||
}
|
|
||||||
|
|
||||||
function trigger_prefetch(event: MouseEvent | TouchEvent) {
|
|
||||||
const a: HTMLAnchorElement = <HTMLAnchorElement>find_anchor(<Node>event.target);
|
|
||||||
if (!a || a.rel !== 'prefetch') return;
|
|
||||||
|
|
||||||
prefetch(a.href);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handle_click(event: MouseEvent) {
|
|
||||||
// Adapted from https://github.com/visionmedia/page.js
|
|
||||||
// MIT license https://github.com/visionmedia/page.js#license
|
|
||||||
if (which(event) !== 1) return;
|
|
||||||
if (event.metaKey || event.ctrlKey || event.shiftKey) return;
|
|
||||||
if (event.defaultPrevented) return;
|
|
||||||
|
|
||||||
const a: HTMLAnchorElement | SVGAElement = <HTMLAnchorElement | SVGAElement>find_anchor(<Node>event.target);
|
|
||||||
if (!a) return;
|
|
||||||
|
|
||||||
if (!a.href) return;
|
|
||||||
|
|
||||||
// check if link is inside an svg
|
|
||||||
// in this case, both href and target are always inside an object
|
|
||||||
const svg = typeof a.href === 'object' && a.href.constructor.name === 'SVGAnimatedString';
|
|
||||||
const href = String(svg ? (<SVGAElement>a).href.baseVal : a.href);
|
|
||||||
|
|
||||||
if (href === location.href) {
|
|
||||||
event.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore if tag has
|
|
||||||
// 1. 'download' attribute
|
|
||||||
// 2. rel='external' attribute
|
|
||||||
if (a.hasAttribute('download') || a.getAttribute('rel') === 'external') return;
|
|
||||||
|
|
||||||
// Ignore if <a> has a target
|
|
||||||
if (svg ? (<SVGAElement>a).target.baseVal : a.target) return;
|
|
||||||
|
|
||||||
const url = new URL(href);
|
|
||||||
|
|
||||||
// Don't handle hash changes
|
|
||||||
if (url.pathname === location.pathname && url.search === location.search) return;
|
|
||||||
|
|
||||||
const target = select_route(url);
|
|
||||||
if (target) {
|
|
||||||
const noscroll = a.hasAttribute('sapper-noscroll')
|
|
||||||
navigate(target, null, noscroll);
|
|
||||||
event.preventDefault();
|
|
||||||
history.pushState({ id: cid }, '', url.href);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function which(event: MouseEvent) {
|
|
||||||
return event.which === null ? event.button : event.which;
|
|
||||||
}
|
|
||||||
|
|
||||||
function find_anchor(node: Node) {
|
|
||||||
while (node && node.nodeName.toUpperCase() !== 'A') node = node.parentNode; // SVG <a> elements have a lowercase name
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handle_popstate(event: PopStateEvent) {
|
|
||||||
scroll_history[cid] = scroll_state();
|
|
||||||
|
|
||||||
if (event.state) {
|
|
||||||
const url = new URL(location.href);
|
|
||||||
const target = select_route(url);
|
|
||||||
if (target) {
|
|
||||||
navigate(target, event.state.id);
|
|
||||||
} else {
|
|
||||||
location.href = location.href;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// hashchange
|
|
||||||
set_uid(uid + 1);
|
|
||||||
set_cid(uid);
|
|
||||||
history.replaceState({ id: cid }, '', location.href);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default as middleware } from './middleware/index';
|
|
||||||
@@ -1,321 +0,0 @@
|
|||||||
import * as fs from 'fs';
|
|
||||||
import * as 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';
|
|
||||||
|
|
||||||
export function get_page_handler(
|
|
||||||
manifest: Manifest,
|
|
||||||
store_getter: (req: Req, res: Res) => Store
|
|
||||||
) {
|
|
||||||
const get_build_info = dev
|
|
||||||
? () => JSON.parse(fs.readFileSync(path.join(build_dir, 'build.json'), 'utf-8'))
|
|
||||||
: (assets => () => assets)(JSON.parse(fs.readFileSync(path.join(build_dir, 'build.json'), 'utf-8')));
|
|
||||||
|
|
||||||
const template = dev
|
|
||||||
? () => read_template(src_dir)
|
|
||||||
: (str => () => str)(read_template(build_dir));
|
|
||||||
|
|
||||||
const has_service_worker = fs.existsSync(path.join(build_dir, 'service-worker.js'));
|
|
||||||
|
|
||||||
const { server_routes, pages } = manifest;
|
|
||||||
const error_route = manifest.error;
|
|
||||||
|
|
||||||
function handle_error(req: Req, res: Res, statusCode: number, error: Error | string) {
|
|
||||||
handle_page({
|
|
||||||
pattern: null,
|
|
||||||
parts: [
|
|
||||||
{ name: null, component: error_route }
|
|
||||||
]
|
|
||||||
}, 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) {
|
|
||||||
const build_info: {
|
|
||||||
bundler: 'rollup' | 'webpack',
|
|
||||||
shimport: string | null,
|
|
||||||
assets: Record<string, string | string[]>,
|
|
||||||
legacy_assets?: Record<string, string>
|
|
||||||
} = get_build_info();
|
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'text/html');
|
|
||||||
res.setHeader('Cache-Control', dev ? 'no-cache' : 'max-age=600');
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
page.parts.forEach(part => {
|
|
||||||
if (!part) return;
|
|
||||||
|
|
||||||
// using concat because it could be a string or an array. thanks webpack!
|
|
||||||
preloaded_chunks = preloaded_chunks.concat(build_info.assets[part.name]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const link = preloaded_chunks
|
|
||||||
.filter(file => file && !file.match(/\.map$/))
|
|
||||||
.map(file => `<${req.baseUrl}/client/${file}>;rel="preload";as="script"`)
|
|
||||||
.join(', ');
|
|
||||||
|
|
||||||
res.setHeader('Link', link);
|
|
||||||
|
|
||||||
const store = store_getter ? store_getter(req, res) : null;
|
|
||||||
|
|
||||||
let redirect: { statusCode: number, location: string };
|
|
||||||
let preload_error: { statusCode: number, message: Error | string };
|
|
||||||
|
|
||||||
const preload_context = {
|
|
||||||
redirect: (statusCode: number, location: string) => {
|
|
||||||
if (redirect && (redirect.statusCode !== statusCode || redirect.location !== location)) {
|
|
||||||
throw new Error(`Conflicting redirects`);
|
|
||||||
}
|
|
||||||
location = location.replace(/^\//g, ''); // leading slash (only)
|
|
||||||
redirect = { statusCode, location };
|
|
||||||
},
|
|
||||||
error: (statusCode: number, message: Error | string) => {
|
|
||||||
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 + '/' :''}`);
|
|
||||||
|
|
||||||
if (opts) {
|
|
||||||
opts = Object.assign({}, opts);
|
|
||||||
|
|
||||||
const include_cookies = (
|
|
||||||
opts.credentials === 'include' ||
|
|
||||||
opts.credentials === 'same-origin' && parsed.origin === `http://127.0.0.1:${process.env.PORT}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (include_cookies) {
|
|
||||||
if (!opts.headers) opts.headers = {};
|
|
||||||
|
|
||||||
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: {}
|
|
||||||
})
|
|
||||||
: {};
|
|
||||||
|
|
||||||
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, {
|
|
||||||
path: req.path,
|
|
||||||
query: req.query,
|
|
||||||
params: part.params ? part.params(match) : {}
|
|
||||||
})
|
|
||||||
: {};
|
|
||||||
}))).catch(err => {
|
|
||||||
preload_error = { statusCode: 500, message: err };
|
|
||||||
return []; // appease TypeScript
|
|
||||||
}).then(preloaded => {
|
|
||||||
if (redirect) {
|
|
||||||
const location = `${req.baseUrl}/${redirect.location}`;
|
|
||||||
|
|
||||||
res.statusCode = redirect.statusCode;
|
|
||||||
res.setHeader('Location', location);
|
|
||||||
res.end();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preload_error) {
|
|
||||||
handle_error(req, res, preload_error.statusCode, preload_error.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const serialized = {
|
|
||||||
preloaded: `[${preloaded.map(data => try_serialize(data)).join(',')}]`,
|
|
||||||
store: store && try_serialize(store.get())
|
|
||||||
};
|
|
||||||
|
|
||||||
const segments = req.path.split('/').filter(Boolean);
|
|
||||||
|
|
||||||
const props: Props = {
|
|
||||||
path: req.path,
|
|
||||||
query: req.query,
|
|
||||||
params: {},
|
|
||||||
child: null
|
|
||||||
};
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
props.error = error instanceof Error ? error : { message: error };
|
|
||||||
props.status = status;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = Object.assign({}, props, preloaded[0], {
|
|
||||||
params: {},
|
|
||||||
child: {
|
|
||||||
segment: segments[0]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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
|
|
||||||
});
|
|
||||||
|
|
||||||
let script = `__SAPPER__={${[
|
|
||||||
error && `error:1`,
|
|
||||||
`baseUrl:"${req.baseUrl}"`,
|
|
||||||
serialized.preloaded && `preloaded:${serialized.preloaded}`,
|
|
||||||
serialized.store && `store:${serialized.store}`
|
|
||||||
].filter(Boolean).join(',')}};`;
|
|
||||||
|
|
||||||
if (has_service_worker) {
|
|
||||||
script += `if('serviceWorker' in navigator)navigator.serviceWorker.register('${req.baseUrl}/service-worker.js');`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = [].concat(build_info.assets.main).filter(file => file && /\.js$/.test(file))[0];
|
|
||||||
const main = `${req.baseUrl}/client/${file}`;
|
|
||||||
|
|
||||||
if (build_info.bundler === 'rollup') {
|
|
||||||
if (build_info.legacy_assets) {
|
|
||||||
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){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}">`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let styles: string;
|
|
||||||
|
|
||||||
// TODO make this consistent across apps
|
|
||||||
// TODO embed build_info in placeholder.ts
|
|
||||||
if (build_info.css && build_info.css.main) {
|
|
||||||
const css_chunks = new Set();
|
|
||||||
if (build_info.css.main) css_chunks.add(build_info.css.main);
|
|
||||||
page.parts.forEach(part => {
|
|
||||||
if (!part) return;
|
|
||||||
const css_chunks_for_part = build_info.css.chunks[part.file];
|
|
||||||
|
|
||||||
if (css_chunks_for_part) {
|
|
||||||
css_chunks_for_part.forEach(file => {
|
|
||||||
css_chunks.add(file);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
styles = Array.from(css_chunks)
|
|
||||||
.map(href => `<link rel="stylesheet" href="client/${href}">`)
|
|
||||||
.join('')
|
|
||||||
} else {
|
|
||||||
styles = (css && css.code ? `<style>${css.code}</style>` : '');
|
|
||||||
}
|
|
||||||
|
|
||||||
// users can set a CSP nonce using res.locals.nonce
|
|
||||||
const nonce_attr = (res.locals && res.locals.nonce) ? ` nonce="${res.locals.nonce}"` : '';
|
|
||||||
|
|
||||||
const body = template()
|
|
||||||
.replace('%sapper.base%', () => `<base href="${req.baseUrl}/">`)
|
|
||||||
.replace('%sapper.scripts%', () => `<script${nonce_attr}>${script}</script>`)
|
|
||||||
.replace('%sapper.html%', () => html)
|
|
||||||
.replace('%sapper.head%', () => `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`)
|
|
||||||
.replace('%sapper.styles%', () => styles);
|
|
||||||
|
|
||||||
res.statusCode = status;
|
|
||||||
res.end(body);
|
|
||||||
}).catch(err => {
|
|
||||||
if (error) {
|
|
||||||
// we encountered an error while rendering the error page — oops
|
|
||||||
res.statusCode = 500;
|
|
||||||
res.end(`<pre>${escape_html(err.message)}</pre>`);
|
|
||||||
} 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handle_error(req, res, 404, 'Not found');
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function read_template(dir = build_dir) {
|
|
||||||
return fs.readFileSync(`${dir}/template.html`, 'utf-8');
|
|
||||||
}
|
|
||||||
|
|
||||||
function try_serialize(data: any) {
|
|
||||||
try {
|
|
||||||
return devalue(data);
|
|
||||||
} catch (err) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function escape_html(html: string) {
|
|
||||||
const chars: Record<string, string> = {
|
|
||||||
'"' : 'quot',
|
|
||||||
"'": '#39',
|
|
||||||
'&': 'amp',
|
|
||||||
'<' : 'lt',
|
|
||||||
'>' : 'gt'
|
|
||||||
};
|
|
||||||
|
|
||||||
return html.replace(/["'&<>]/g, c => `&${chars[c]};`);
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import { IGNORE } from '../placeholders';
|
|
||||||
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) {
|
|
||||||
req.params = route.params(route.pattern.exec(req.path));
|
|
||||||
|
|
||||||
const method = req.method.toLowerCase();
|
|
||||||
// 'delete' cannot be exported from a module because it is a keyword,
|
|
||||||
// so check for 'del' instead
|
|
||||||
const method_export = method === 'delete' ? 'del' : method;
|
|
||||||
const handle_method = route.handlers[method_export];
|
|
||||||
if (handle_method) {
|
|
||||||
if (process.env.SAPPER_EXPORT) {
|
|
||||||
const { write, end, setHeader } = res;
|
|
||||||
const chunks: any[] = [];
|
|
||||||
const headers: Record<string, string> = {};
|
|
||||||
|
|
||||||
// intercept data so that it can be exported
|
|
||||||
res.write = function(chunk: any) {
|
|
||||||
chunks.push(Buffer.from(chunk));
|
|
||||||
write.apply(res, arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
res.setHeader = function(name: string, value: string) {
|
|
||||||
headers[name.toLowerCase()] = value;
|
|
||||||
setHeader.apply(res, arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
res.end = function(chunk?: any) {
|
|
||||||
if (chunk) chunks.push(Buffer.from(chunk));
|
|
||||||
end.apply(res, arguments);
|
|
||||||
|
|
||||||
process.send({
|
|
||||||
__sapper__: true,
|
|
||||||
event: 'file',
|
|
||||||
url: req.url,
|
|
||||||
method: req.method,
|
|
||||||
status: res.statusCode,
|
|
||||||
type: headers['content-type'],
|
|
||||||
body: Buffer.concat(chunks).toString()
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const handle_next = (err?: Error) => {
|
|
||||||
if (err) {
|
|
||||||
res.statusCode = 500;
|
|
||||||
res.end(err.message);
|
|
||||||
} else {
|
|
||||||
process.nextTick(next);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
handle_method(req, res, handle_next);
|
|
||||||
} catch (err) {
|
|
||||||
handle_next(err);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// no matching handler for method
|
|
||||||
process.nextTick(next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return function find_route(req: Req, res: Res, next: () => void) {
|
|
||||||
if (req[IGNORE]) return next();
|
|
||||||
|
|
||||||
for (const route of routes) {
|
|
||||||
if (route.pattern.test(req.path)) {
|
|
||||||
handle_route(route, req, res, next);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
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 { get_server_route_handler } from './get_server_route_handler';
|
|
||||||
import { get_page_handler } from './get_page_handler';
|
|
||||||
import { lookup } from './mime';
|
|
||||||
|
|
||||||
export default function middleware(opts: {
|
|
||||||
store?: (req: Req, res: Res) => Store,
|
|
||||||
ignore?: any
|
|
||||||
} = {}) {
|
|
||||||
const { store, ignore } = opts;
|
|
||||||
|
|
||||||
let emitted_basepath = false;
|
|
||||||
|
|
||||||
return compose_handlers([
|
|
||||||
ignore && ((req: Req, res: Res, next: () => void) => {
|
|
||||||
req[IGNORE] = should_ignore(req.path, ignore);
|
|
||||||
next();
|
|
||||||
}),
|
|
||||||
|
|
||||||
(req: Req, res: Res, next: () => void) => {
|
|
||||||
if (req[IGNORE]) return next();
|
|
||||||
|
|
||||||
if (req.baseUrl === undefined) {
|
|
||||||
let { originalUrl } = req;
|
|
||||||
if (req.url === '/' && originalUrl[originalUrl.length - 1] !== '/') {
|
|
||||||
originalUrl += '/';
|
|
||||||
}
|
|
||||||
|
|
||||||
req.baseUrl = originalUrl
|
|
||||||
? originalUrl.slice(0, -req.url.length)
|
|
||||||
: '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!emitted_basepath && process.send) {
|
|
||||||
process.send({
|
|
||||||
__sapper__: true,
|
|
||||||
event: 'basepath',
|
|
||||||
basepath: req.baseUrl
|
|
||||||
});
|
|
||||||
|
|
||||||
emitted_basepath = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.path === undefined) {
|
|
||||||
req.path = req.url.replace(/\?.*/, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
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'
|
|
||||||
}),
|
|
||||||
|
|
||||||
fs.existsSync(path.join(build_dir, 'service-worker.js.map')) && serve({
|
|
||||||
pathname: '/service-worker.js.map',
|
|
||||||
cache_control: 'no-cache, no-store, must-revalidate'
|
|
||||||
}),
|
|
||||||
|
|
||||||
serve({
|
|
||||||
prefix: '/client/',
|
|
||||||
cache_control: dev ? 'no-cache' : 'max-age=31536000, immutable'
|
|
||||||
}),
|
|
||||||
|
|
||||||
get_server_route_handler(manifest.server_routes),
|
|
||||||
|
|
||||||
get_page_handler(manifest, store)
|
|
||||||
].filter(Boolean));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function compose_handlers(handlers: Handler[]) {
|
|
||||||
return (req: Req, res: Res, next: () => void) => {
|
|
||||||
let i = 0;
|
|
||||||
function go() {
|
|
||||||
const handler = handlers[i];
|
|
||||||
|
|
||||||
if (handler) {
|
|
||||||
handler(req, res, () => {
|
|
||||||
i += 1;
|
|
||||||
go();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
go();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function should_ignore(uri: string, val: any) {
|
|
||||||
if (Array.isArray(val)) return val.some(x => should_ignore(uri, x));
|
|
||||||
if (val instanceof RegExp) return val.test(uri);
|
|
||||||
if (typeof val === 'function') return val(uri);
|
|
||||||
return uri.startsWith(val.charCodeAt(0) === 47 ? val : `/${val}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function serve({ prefix, pathname, cache_control }: {
|
|
||||||
prefix?: string,
|
|
||||||
pathname?: string,
|
|
||||||
cache_control: string
|
|
||||||
}) {
|
|
||||||
const filter = pathname
|
|
||||||
? (req: Req) => req.path === pathname
|
|
||||||
: (req: Req) => req.path.startsWith(prefix);
|
|
||||||
|
|
||||||
const cache: Map<string, Buffer> = new Map();
|
|
||||||
|
|
||||||
const read = dev
|
|
||||||
? (file: string) => fs.readFileSync(path.resolve(build_dir, file))
|
|
||||||
: (file: string) => (cache.has(file) ? cache : cache.set(file, fs.readFileSync(path.resolve(build_dir, file)))).get(file)
|
|
||||||
|
|
||||||
return (req: Req, res: Res, next: () => void) => {
|
|
||||||
if (req[IGNORE]) return next();
|
|
||||||
|
|
||||||
if (filter(req)) {
|
|
||||||
const type = lookup(req.path);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const file = decodeURIComponent(req.path.slice(1));
|
|
||||||
const data = read(file);
|
|
||||||
|
|
||||||
res.setHeader('Content-Type', type);
|
|
||||||
res.setHeader('Cache-Control', cache_control);
|
|
||||||
res.end(data);
|
|
||||||
} catch (err) {
|
|
||||||
res.statusCode = 404;
|
|
||||||
res.end('not found');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import { ClientRequest, ServerResponse } from 'http';
|
|
||||||
|
|
||||||
export type ServerRoute = {
|
|
||||||
pattern: RegExp;
|
|
||||||
handlers: Record<string, Handler>;
|
|
||||||
params: (match: RegExpMatchArray) => Record<string, string>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Page = {
|
|
||||||
pattern: RegExp;
|
|
||||||
parts: Array<{
|
|
||||||
name: string;
|
|
||||||
component: Component;
|
|
||||||
params?: (match: RegExpMatchArray) => Record<string, string>;
|
|
||||||
}>
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Manifest = {
|
|
||||||
server_routes: ServerRoute[];
|
|
||||||
pages: Page[];
|
|
||||||
root: Component;
|
|
||||||
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: {
|
|
||||||
segment: string;
|
|
||||||
component: Component;
|
|
||||||
props: Props;
|
|
||||||
};
|
|
||||||
[key: string]: any;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface Req extends ClientRequest {
|
|
||||||
url: string;
|
|
||||||
baseUrl: string;
|
|
||||||
originalUrl: string;
|
|
||||||
method: string;
|
|
||||||
path: string;
|
|
||||||
params: Record<string, string>;
|
|
||||||
query: Record<string, string>;
|
|
||||||
headers: Record<string, string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Res extends ServerResponse {
|
|
||||||
write: (data: any) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { ServerResponse };
|
|
||||||
|
|
||||||
interface Component {
|
|
||||||
render: (data: any, opts: { store: Store }) => {
|
|
||||||
head: string;
|
|
||||||
css: { code: string, map: any };
|
|
||||||
html: string
|
|
||||||
},
|
|
||||||
preload: (data: any) => any | Promise<any>
|
|
||||||
}
|
|
||||||
@@ -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__';
|
|
||||||
13
test/app/app/client.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { init, prefetchRoutes } from '../../../runtime.js';
|
||||||
|
import { Store } from 'svelte/store.js';
|
||||||
|
import { manifest } from './manifest/client.js';
|
||||||
|
|
||||||
|
window.init = () => {
|
||||||
|
return init({
|
||||||
|
target: document.querySelector('#sapper'),
|
||||||
|
manifest,
|
||||||
|
store: data => new Store(data)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.prefetchRoutes = prefetchRoutes;
|
||||||
@@ -2,8 +2,9 @@ import fs from 'fs';
|
|||||||
import { resolve } from 'url';
|
import { resolve } from 'url';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import serve from 'serve-static';
|
import serve from 'serve-static';
|
||||||
|
import sapper from '../../../dist/middleware.ts.js';
|
||||||
import { Store } from 'svelte/store.js';
|
import { Store } from 'svelte/store.js';
|
||||||
import * as sapper from '../__sapper__/server.js';
|
import { manifest } from './manifest/server.js';
|
||||||
|
|
||||||
let pending;
|
let pending;
|
||||||
let ended;
|
let ended;
|
||||||
@@ -44,7 +45,7 @@ const middlewares = [
|
|||||||
|
|
||||||
// set test cookie
|
// set test cookie
|
||||||
(req, res, next) => {
|
(req, res, next) => {
|
||||||
res.setHeader('Set-Cookie', ['a=1; Path=/', 'b=2; Path=/']);
|
res.setHeader('Set-Cookie', 'test=woohoo!; Max-Age=3600');
|
||||||
next();
|
next();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -84,17 +85,11 @@ const middlewares = [
|
|||||||
next();
|
next();
|
||||||
},
|
},
|
||||||
|
|
||||||
// set up some values for the store
|
sapper({
|
||||||
(req, res, next) => {
|
manifest,
|
||||||
req.hello = 'hello';
|
store: () => {
|
||||||
res.locals = { name: 'world' };
|
|
||||||
next();
|
|
||||||
},
|
|
||||||
|
|
||||||
sapper.middleware({
|
|
||||||
store: (req, res) => {
|
|
||||||
return new Store({
|
return new Store({
|
||||||
title: `${req.hello} ${res.locals.name}`
|
title: 'Stored title'
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
ignore: [
|
ignore: [
|
||||||
@@ -106,13 +101,6 @@ const middlewares = [
|
|||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
app.get(`${BASEPATH}/non-sapper-redirect-from`, (req, res) => {
|
|
||||||
res.writeHead(301, {
|
|
||||||
Location: `${BASEPATH}/non-sapper-redirect-to`
|
|
||||||
});
|
|
||||||
res.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (BASEPATH) {
|
if (BASEPATH) {
|
||||||
app.use(BASEPATH, ...middlewares);
|
app.use(BASEPATH, ...middlewares);
|
||||||
} else {
|
} else {
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { files, shell, timestamp, routes } from '../__sapper__/service-worker.js';
|
import { assets, shell, timestamp, routes } from './manifest/service-worker.js';
|
||||||
|
|
||||||
const ASSETS = `cachetimestamp`;
|
const ASSETS = `cachetimestamp`;
|
||||||
|
|
||||||
// `shell` is an array of all the files generated by webpack,
|
// `shell` is an array of all the files generated by webpack,
|
||||||
// `assets` is an array of everything in the `assets` directory
|
// `assets` is an array of everything in the `assets` directory
|
||||||
const to_cache = shell.concat(files);
|
const to_cache = shell.concat(assets);
|
||||||
const cached = new Set(to_cache);
|
const cached = new Set(to_cache);
|
||||||
|
|
||||||
self.addEventListener('install', event => {
|
self.addEventListener('install', event => {
|
||||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
@@ -9,9 +9,17 @@
|
|||||||
<button class='prefetch' on:click='prefetch("blog/why-the-name")'>Why the name?</button>
|
<button class='prefetch' on:click='prefetch("blog/why-the-name")'>Why the name?</button>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { prefetch } from '../../__sapper__/client.js';
|
import { goto, prefetch } from '../../../runtime.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
oncreate() {
|
||||||
|
window.goto = goto;
|
||||||
|
},
|
||||||
|
|
||||||
|
ondestroy() {
|
||||||
|
window.goto = null;
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
prefetch
|
prefetch
|
||||||
}
|
}
|
||||||
@@ -97,9 +97,6 @@ const posts = [
|
|||||||
<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>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>
|
<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>
|
||||||
|
|
||||||
<p><a href="blog/another-long-post">clicking this link should reset scroll</a></p>
|
|
||||||
<p><a href="blog/another-long-post" sapper-noscroll>clicking this link should not affect scroll</a></p>
|
|
||||||
|
|
||||||
<h2 id='three'>Three</h2>
|
<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>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>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>
|
||||||
@@ -109,38 +106,6 @@ const posts = [
|
|||||||
<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>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>
|
<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: 'Another long post',
|
|
||||||
slug: 'another-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>
|
|
||||||
`
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
28
test/app/routes/credentials/test.json.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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
test/app/routes/fünke.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<h1>I'm afraid I just blue myself</h1>
|
||||||
@@ -7,16 +7,11 @@
|
|||||||
<a href='.'>home</a>
|
<a href='.'>home</a>
|
||||||
<a href='about'>about</a>
|
<a href='about'>about</a>
|
||||||
<a href='slow-preload'>slow preload</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-from'>redirect</a>
|
||||||
<a href='redirect-root'>redirect (root)</a>
|
|
||||||
<a href='blog/nope'>broken link</a>
|
<a href='blog/nope'>broken link</a>
|
||||||
<a href='blog/throw-an-error'>error link</a>
|
<a href='blog/throw-an-error'>error link</a>
|
||||||
<a href='credentials?creds=include'>credentials</a>
|
<a href='credentials?creds=include'>credentials</a>
|
||||||
<a rel=prefetch class='{page === "blog" ? "selected" : ""}' href='blog'>blog</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>
|
<div class='hydrate-test'></div>
|
||||||
|
|
||||||