Compare commits

..

43 Commits

Author SHA1 Message Date
Rich Harris
3b714c0de3 -> v0.23.5 2018-10-24 21:40:04 -04:00
Rich Harris
28186227a9 add tests 2018-10-24 21:20:27 -04:00
Rich Harris
2ac0f2bf3d Merge branch 'search-params-decoding' of https://github.com/mrkishi/sapper into mrkishi-search-params-decoding 2018-10-24 21:10:35 -04:00
Rich Harris
4991f3b359 support non-native promise implementations 2018-10-24 21:05:25 -04:00
Rich Harris
65128118c7 Merge branch '487-async-route-errors' of https://github.com/nikku/sapper into nikku-487-async-route-errors 2018-10-24 20:58:52 -04:00
Rich Harris
3eced6fa4d Merge pull request #492 from sveltejs/lazy-css
fix lazy css bug, add tests
2018-10-24 20:58:08 -04:00
mrkishi
c4aee66c32 Fix search params decoding 2018-10-24 21:19:03 -03:00
Rich Harris
410c52df41 fix lazy css bug, add tests 2018-10-24 18:48:38 -04:00
Rich Harris
ffd56e2a20 -> v0.23.4 2018-10-24 15:51:44 -04:00
Rich Harris
1e5a87cf71 Merge pull request #491 from sveltejs/empty-href-export
ignore empty anchors when exporting
2018-10-24 15:51:28 -04:00
Rich Harris
281e183c61 ignore empty anchors when exporting 2018-10-24 15:38:45 -04:00
Nico Rehwaldt
3fe7b55955 async -> Promise.reject 2018-10-20 22:46:42 +02:00
Nico Rehwaldt
464924ed67 handle async route errors
Related to #487
2018-10-20 22:40:21 +02:00
Rich Harris
e5d7d8ab2b -> v0.23.3 2018-10-16 16:16:11 -04:00
Rich Harris
d3e560325d Merge pull request #477 from sveltejs/clear-errors
clear errors on successful render
2018-10-16 16:15:28 -04:00
Rich Harris
64e5065aa5 clear errors on successful render 2018-10-16 15:59:57 -04:00
Rich Harris
cb45bb0fbe -> v0.23.2 2018-10-16 08:58:02 -04:00
Rich Harris
f39455014a update deps 2018-10-16 08:44:22 -04:00
Rich Harris
4fe8df3696 Merge pull request #471 from sveltejs/missing-css
include css depended upon by entry point, even if also depended on by a lazily-loaded component
2018-10-16 08:38:20 -04:00
Rich Harris
4fdc7055c1 -> v0.23.1 2018-10-15 22:10:18 -04:00
Rich Harris
cca417a85a simplify, preserve monomorphism 2018-10-15 22:07:23 -04:00
Rich Harris
635c13a175 Merge branch 'tags-from-other-pages' of https://github.com/DayBr3ak/sapper into DayBr3ak-tags-from-other-pages 2018-10-15 21:43:39 -04:00
Rich Harris
2e3aef8b21 simplify 2018-10-15 21:36:40 -04:00
Rich Harris
44736754ad fix file extension 2018-10-15 21:27:52 -04:00
Benjamin GROENEVELD
a399d87d9b handle tag click from another page 2018-10-13 21:59:47 +02:00
Benjamin GROENEVELD
a68c62ce91 Fix hash link reliability (fix #434) 2018-10-13 16:45:14 +02:00
Rich Harris
1b9b559d82 include css depended upon by entry point, even if also depended on by a lazily-loaded component 2018-10-11 23:18:51 -04:00
Rich Harris
abcac75826 -> v0.23.0 2018-10-09 15:03:02 -04:00
Rich Harris
4118c566d1 Merge pull request #470 from aubergene/ssi
Allow SSI comments
2018-10-09 15:02:21 -04:00
Rich Harris
0b76f12394 Merge pull request #469 from sveltejs/remove-env-vars
replace magic env vars with documented CLI flags
2018-10-09 14:59:30 -04:00
Julian Burgess
e51cb85c7c Allow SSI directives 2018-10-09 19:50:25 +01:00
Rich Harris
6ae9a5e7c5 replace magic env vars with documented CLI flags 2018-10-09 08:33:25 -04:00
Rich Harris
52f40f9e63 Improve internal API 2018-10-08 19:21:15 -04:00
Rich Harris
5e59855a15 overhaul tests 2018-10-07 18:23:43 -04:00
Rich Harris
18acef3190 -> v0.22.10 2018-10-05 21:25:49 -04:00
Rich Harris
d7f6ca8b4d Merge pull request #466 from sveltejs/css-basepath
no need to use basepath in <link>
2018-10-05 21:22:15 -04:00
Rich Harris
00321932ef no need to use basepath in <link> 2018-10-05 21:16:27 -04:00
Rich Harris
7eb1ec727c add tests for #376 2018-10-05 20:59:45 -04:00
Rich Harris
3f586e19a1 minor tweaks 2018-10-05 20:57:15 -04:00
Daniil Khanin
05b702938f implement sapper no scroll 2018-10-05 00:13:08 +03:00
Rich Harris
3026e7c36e remove some leftover logging 2018-10-02 11:39:38 -04:00
Rich Harris
27a5aed83e -> v0.22.9 2018-10-02 11:13:36 -04:00
Rich Harris
bb04af41bd fix legacy builds 2018-10-02 11:13:09 -04:00
227 changed files with 5285 additions and 4309 deletions

View File

@@ -1,5 +1,44 @@
# sapper changelog
## 0.23.5
* Include lazily-imported CSS in main CSS chunk ([#492](https://github.com/sveltejs/sapper/pull/492))
* Make search param decoding spec-compliant ([#493](https://github.com/sveltejs/sapper/pull/493))
* Handle async route errors ([#488](https://github.com/sveltejs/sapper/pull/488))
## 0.23.4
* Ignore empty anchors when exporting ([#491](https://github.com/sveltejs/sapper/pull/491))
## 0.23.3
* Clear `error` and `status` on successful render ([#477](https://github.com/sveltejs/sapper/pull/477))
## 0.23.2
* Fix entry point CSS ([#471](https://github.com/sveltejs/sapper/pull/471))
## 0.23.1
* Scroll to deeplink that matches current URL ([#472](https://github.com/sveltejs/sapper/pull/472))
* Scroll to deeplink on another page ([#341](https://github.com/sveltejs/sapper/issues/341))
## 0.23.0
* Overhaul internal APIs ([#468](https://github.com/sveltejs/sapper/pull/468))
* Remove unused `sapper start` and `sapper upgrade` ([#468](https://github.com/sveltejs/sapper/pull/468))
* Remove magic environment variables ([#469](https://github.com/sveltejs/sapper/pull/469))
* Preserve SSI comments ([#470](https://github.com/sveltejs/sapper/pull/470))
## 0.22.10
* Handle `sapper-noscroll` attribute on `<a>` elements ([#376](https://github.com/sveltejs/sapper/issues/376))
* Fix CSS paths when using a base path ([#466](https://github.com/sveltejs/sapper/pull/466))
## 0.22.9
* Fix legacy builds ([#462](https://github.com/sveltejs/sapper/pull/462))
## 0.22.8
* Ensure CSS placeholders are overwritten ([#462](https://github.com/sveltejs/sapper/pull/462))

View File

@@ -1,9 +0,0 @@
{
"baseUrl": "http://localhost:3000",
"videoRecording": false,
"fixturesFolder": "test/cypress/fixtures",
"integrationFolder": "test/cypress/integration",
"pluginsFile": false,
"screenshotsFolder": "test/cypress/screenshots",
"supportFile": "test/cypress/support/index.js"
}

View File

@@ -2,4 +2,4 @@
--require ts-node/register
--recursive
test/unit/*/*.ts
test/common/test.js
test/apps/*/test.ts

2344
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "sapper",
"version": "0.22.8",
"version": "0.23.5",
"description": "Military-grade apps, engineered by Svelte",
"bin": {
"sapper": "./sapper"
@@ -17,66 +17,62 @@
"test": "test"
},
"dependencies": {
"html-minifier": "^3.5.16",
"html-minifier": "^3.5.20",
"shimport": "0.0.11",
"source-map-support": "^0.5.6",
"sourcemap-codec": "^1.4.1",
"source-map-support": "^0.5.9",
"sourcemap-codec": "^1.4.3",
"string-hash": "^1.1.3",
"tslib": "^1.9.1"
"tslib": "^1.9.3"
},
"devDependencies": {
"@types/glob": "^5.0.34",
"@types/mkdirp": "^0.5.2",
"@types/mocha": "^5.2.5",
"@types/node": "^10.7.1",
"@types/node": "^10.12.0",
"@types/puppeteer": "^1.9.0",
"@types/rimraf": "^2.0.2",
"agadoo": "^1.0.1",
"cheap-watch": "^0.3.0",
"compression": "^1.7.1",
"cheap-watch": "^1.0.0",
"cookie": "^0.3.1",
"devalue": "^1.0.4",
"eslint": "^4.13.1",
"eslint-plugin-import": "^2.12.0",
"express": "^4.16.3",
"kleur": "^2.0.1",
"eslint": "^5.7.0",
"eslint-plugin-import": "^2.14.0",
"kleur": "^2.0.2",
"mkdirp": "^0.5.1",
"mocha": "^5.2.0",
"nightmare": "^3.0.0",
"node-fetch": "^2.1.1",
"node-fetch": "^2.2.0",
"npm-run-all": "^4.1.3",
"polka": "^0.4.0",
"polka": "^0.5.1",
"port-authority": "^1.0.5",
"pretty-bytes": "^5.0.0",
"pretty-ms": "^3.1.0",
"pretty-bytes": "^5.1.0",
"puppeteer": "^1.9.0",
"require-relative": "^0.8.7",
"rimraf": "^2.6.2",
"rollup": "^0.65.0",
"rollup-plugin-commonjs": "^9.1.3",
"rollup-plugin-json": "^3.0.0",
"rollup-plugin-node-resolve": "^3.3.0",
"rollup": "^0.66.6",
"rollup-plugin-commonjs": "^9.2.0",
"rollup-plugin-json": "^3.1.0",
"rollup-plugin-node-resolve": "^3.4.0",
"rollup-plugin-replace": "^2.1.0",
"rollup-plugin-string": "^2.0.2",
"rollup-plugin-typescript": "^0.8.1",
"rollup-plugin-svelte": "^4.3.2",
"rollup-plugin-typescript": "^1.0.0",
"sade": "^1.4.1",
"sander": "^0.6.0",
"serve-static": "^1.13.2",
"svelte": "^2.6.3",
"svelte-loader": "^2.9.0",
"tiny-glob": "^0.2.2",
"sirv": "^0.2.2",
"svelte": "^2.13.5",
"svelte-loader": "^2.11.0",
"ts-node": "^7.0.1",
"typescript": "^2.8.3",
"walk-sync": "^0.3.2",
"webpack": "^4.8.3",
"webpack-format-messages": "^2.0.1"
"typescript": "^3.1.3",
"webpack": "^4.20.2",
"webpack-format-messages": "^2.0.3"
},
"scripts": {
"cy:open": "cypress open",
"test": "mocha --opts mocha.opts",
"pretest": "npm run build",
"build": "rm -rf dist && rollup -c",
"prepare": "npm run build",
"dev": "rollup -cw",
"prepublishOnly": "npm test",
"update_mime_types": "curl http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types | grep -e \"^[^#]\" > src/middleware/mime-types.md"
"update_mime_types": "curl http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types | grep -e \"^[^#]\" > templates/src/server/middleware/mime-types.md"
},
"repository": "https://github.com/sveltejs/sapper",
"keywords": [

View File

@@ -12,7 +12,7 @@ const external = [].concat(
'sapper/core.js'
);
function template(kind, external) {
function template(kind, external, target) {
return {
input: `templates/src/${kind}/index.ts`,
output: {
@@ -28,23 +28,23 @@ function template(kind, external) {
}),
typescript({
typescript: require('typescript'),
target: "ES2017"
target
})
]
};
}
export default [
template('client', ['__ROOT__', '__ERROR__']),
template('server', builtinModules),
template('client', ['__ROOT__', '__ERROR__'], 'ES2017'),
template('server', builtinModules, 'ES2015'),
{
input: [
`src/api.ts`,
`src/cli.ts`,
`src/core.ts`,
`src/rollup.ts`,
`src/webpack.ts`
`src/config/rollup.ts`,
`src/config/webpack.ts`
],
output: {
dir: 'dist',

View File

@@ -1,6 +1,4 @@
import { dev } from './api/dev';
import { build } from './api/build';
import { exporter } from './api/export';
import { find_page } from './api/find_page';
export { dev, build, exporter, find_page };
export { dev } from './api/dev';
export { build } from './api/build';
export { export } from './api/export';
export { find_page } from './api/find_page';

View File

@@ -2,44 +2,59 @@ import * as fs from 'fs';
import * as path from 'path';
import mkdirp from 'mkdirp';
import rimraf from 'rimraf';
import { EventEmitter } from 'events';
import minify_html from './utils/minify_html';
import { create_compilers, create_main_manifests, create_manifest_data, create_serviceworker_manifest } from '../core';
import * as events from './interfaces';
import { copy_shimport } from './utils/copy_shimport';
import { Dirs } from '../interfaces';
import read_template from '../core/read_template';
import { CompileResult } from '../core/create_compilers/interfaces';
import { noop } from './utils/noop';
import validate_bundler from './utils/validate_bundler';
type Opts = {
legacy: boolean;
bundler: 'rollup' | 'webpack';
cwd?: string;
src?: string;
routes?: string;
dest?: string;
output?: string;
static?: string;
legacy?: boolean;
bundler?: 'rollup' | 'webpack';
oncompile?: ({ type, result }: { type: string, result: CompileResult }) => void;
};
export function build(opts: Opts, dirs: Dirs) {
const emitter = new EventEmitter();
export async function build({
cwd,
src = 'src',
routes = 'src/routes',
output = '__sapper__',
static: static_files = 'static',
dest = '__sapper__/build',
execute(emitter, opts, dirs).then(
() => {
emitter.emit('done', <events.DoneEvent>{}); // TODO do we need to pass back any info?
},
error => {
emitter.emit('error', <events.ErrorEvent>{
error
});
}
);
bundler,
legacy = false,
oncompile = noop
}: Opts = {}) {
bundler = validate_bundler(bundler);
return emitter;
}
cwd = path.resolve(cwd);
src = path.resolve(cwd, src);
dest = path.resolve(cwd, dest);
routes = path.resolve(cwd, routes);
output = path.resolve(cwd, output);
static_files = path.resolve(cwd, static_files);
dest = path.resolve(cwd, dest);
async function execute(emitter: EventEmitter, opts: Opts, dirs: Dirs) {
rimraf.sync(path.join(dirs.dest, '**/*'));
mkdirp.sync(`${dirs.dest}/client`);
copy_shimport(dirs.dest);
if (legacy && bundler === 'webpack') {
throw new Error(`Legacy builds are not supported for projects using webpack`);
}
rimraf.sync(path.join(dest, '**/*'));
mkdirp.sync(`${dest}/client`);
copy_shimport(dest);
// minify src/template.html
// TODO compile this to a function? could be quicker than str.replace(...).replace(...).replace(...)
const template = read_template();
const template = read_template(src);
// remove this in a future version
if (template.indexOf('%sapper.base%') === -1) {
@@ -48,47 +63,53 @@ async function execute(emitter: EventEmitter, opts: Opts, dirs: Dirs) {
throw error;
}
fs.writeFileSync(`${dirs.dest}/template.html`, minify_html(template));
fs.writeFileSync(`${dest}/template.html`, minify_html(template));
const manifest_data = create_manifest_data();
const manifest_data = create_manifest_data(routes);
// create src/manifest/client.js and src/manifest/server.js
create_main_manifests({ bundler: opts.bundler, manifest_data });
create_main_manifests({
bundler,
manifest_data,
cwd,
src,
dest,
routes,
output,
dev: false
});
const { client, server, serviceworker } = await create_compilers(opts.bundler);
const { client, server, serviceworker } = await create_compilers(bundler, cwd, src, dest, true);
const client_result = await client.compile();
emitter.emit('build', <events.BuildEvent>{
oncompile({
type: 'client',
// TODO duration/warnings
result: client_result
});
const build_info = client_result.to_json(manifest_data, dirs);
const build_info = client_result.to_json(manifest_data, { src, routes, dest });
if (opts.legacy) {
if (legacy) {
process.env.SAPPER_LEGACY_BUILD = 'true';
const { client } = await create_compilers(opts.bundler);
const { client } = await create_compilers(bundler, cwd, src, dest, true);
const client_result = await client.compile();
emitter.emit('build', <events.BuildEvent>{
oncompile({
type: 'client (legacy)',
// TODO duration/warnings
result: client_result
});
client_result.to_json(manifest_data, dirs);
client_result.to_json(manifest_data, { src, routes, dest });
build_info.legacy_assets = client_result.assets;
delete process.env.SAPPER_LEGACY_BUILD;
}
fs.writeFileSync(path.join(dirs.dest, 'build.json'), JSON.stringify(build_info));
fs.writeFileSync(path.join(dest, 'build.json'), JSON.stringify(build_info));
const server_stats = await server.compile();
emitter.emit('build', <events.BuildEvent>{
oncompile({
type: 'server',
// TODO duration/warnings
result: server_stats
});
@@ -97,14 +118,15 @@ async function execute(emitter: EventEmitter, opts: Opts, dirs: Dirs) {
if (serviceworker) {
create_serviceworker_manifest({
manifest_data,
client_files: client_result.chunks.map(chunk => `client/${chunk.file}`)
output,
client_files: client_result.chunks.map(chunk => `client/${chunk.file}`),
static_files
});
serviceworker_stats = await serviceworker.compile();
emitter.emit('build', <events.BuildEvent>{
oncompile({
type: 'serviceworker',
// TODO duration/warnings
result: serviceworker_stats
});
}

View File

@@ -5,30 +5,45 @@ import * as child_process from 'child_process';
import * as ports from 'port-authority';
import mkdirp from 'mkdirp';
import rimraf from 'rimraf';
import { locations } from '../config';
import { EventEmitter } from 'events';
import { create_manifest_data, create_main_manifests, create_compilers, create_serviceworker_manifest } from '../core';
import { Compiler, Compilers } from '../core/create_compilers';
import { CompileResult, CompileError } from '../core/create_compilers/interfaces';
import { CompileResult } from '../core/create_compilers/interfaces';
import Deferred from './utils/Deferred';
import * as events from './interfaces';
import validate_bundler from '../cli/utils/validate_bundler';
import validate_bundler from './utils/validate_bundler';
import { copy_shimport } from './utils/copy_shimport';
import { ManifestData } from '../interfaces';
import { ManifestData, FatalEvent, ErrorEvent, ReadyEvent, InvalidEvent } from '../interfaces';
import read_template from '../core/read_template';
import { noop } from './utils/noop';
export function dev(opts) {
type Opts = {
cwd?: string,
src?: string,
dest?: string,
routes?: string,
output?: string,
static?: string,
'dev-port'?: number,
live?: boolean,
hot?: boolean,
'devtools-port'?: number,
bundler?: 'rollup' | 'webpack',
port?: number
};
export function dev(opts: Opts) {
return new Watcher(opts);
}
class Watcher extends EventEmitter {
bundler: string;
bundler: 'rollup' | 'webpack';
dirs: {
cwd: string;
src: string;
dest: string;
routes: string;
rollup: string;
webpack: string;
output: string;
static: string;
}
port: number;
closed: boolean;
@@ -54,34 +69,33 @@ class Watcher extends EventEmitter {
}
constructor({
src = locations.src(),
dest = locations.dest(),
routes = locations.routes(),
cwd = '.',
src = 'src',
routes = 'src/routes',
output = '__sapper__',
static: static_files = 'static',
dest = '__sapper__/dev',
'dev-port': dev_port,
live,
hot,
'devtools-port': devtools_port,
bundler,
webpack = 'webpack',
rollup = 'rollup',
port = +process.env.PORT
}: {
src: string,
dest: string,
routes: string,
'dev-port': number,
live: boolean,
hot: boolean,
'devtools-port': number,
bundler?: string,
webpack: string,
rollup: string,
port: number
}) {
}: Opts) {
super();
cwd = path.resolve(cwd);
this.bundler = validate_bundler(bundler);
this.dirs = { src, dest, routes, webpack, rollup };
this.dirs = {
cwd,
src: path.resolve(cwd, src),
dest: path.resolve(cwd, dest),
routes: path.resolve(cwd, routes),
output: path.resolve(cwd, output),
static: path.resolve(cwd, static_files)
};
this.port = port;
this.closed = false;
@@ -101,7 +115,7 @@ class Watcher extends EventEmitter {
};
// remove this in a future version
const template = read_template();
const template = read_template(src);
if (template.indexOf('%sapper.base%') === -1) {
const error = new Error(`As of Sapper v0.10, your template.html file must include %sapper.base% in the <head>`);
error.code = `missing-sapper-base`;
@@ -120,7 +134,7 @@ class Watcher extends EventEmitter {
async init() {
if (this.port) {
if (!await ports.check(this.port)) {
this.emit('fatal', <events.FatalEvent>{
this.emit('fatal', <FatalEvent>{
message: `Port ${this.port} is unavailable`
});
return;
@@ -129,7 +143,7 @@ class Watcher extends EventEmitter {
this.port = await ports.find(3000);
}
const { dest } = this.dirs;
const { cwd, src, dest, routes, output, static: static_files } = this.dirs;
rimraf.sync(dest);
mkdirp.sync(`${dest}/client`);
if (this.bundler === 'rollup') copy_shimport(dest);
@@ -142,10 +156,16 @@ class Watcher extends EventEmitter {
let manifest_data: ManifestData;
try {
manifest_data = create_manifest_data();
create_main_manifests({ bundler: this.bundler, manifest_data, dev_port: this.dev_port });
manifest_data = create_manifest_data(routes);
create_main_manifests({
bundler: this.bundler,
manifest_data,
dev: true,
dev_port: this.dev_port,
cwd, src, dest, routes, output
});
} catch (err) {
this.emit('fatal', <events.FatalEvent>{
this.emit('fatal', <FatalEvent>{
message: err.message
});
return;
@@ -155,7 +175,7 @@ class Watcher extends EventEmitter {
this.filewatchers.push(
watch_dir(
locations.routes(),
routes,
({ path: file, stats }) => {
if (stats.isDirectory()) {
return path.basename(file)[0] !== '_';
@@ -164,19 +184,25 @@ class Watcher extends EventEmitter {
},
() => {
try {
const new_manifest_data = create_manifest_data();
create_main_manifests({ bundler: this.bundler, manifest_data, dev_port: this.dev_port });
const new_manifest_data = create_manifest_data(routes);
create_main_manifests({
bundler: this.bundler,
manifest_data, // TODO is this right? not new_manifest_data?
dev: true,
dev_port: this.dev_port,
cwd, src, dest, routes, output
});
manifest_data = new_manifest_data;
} catch (err) {
this.emit('error', <events.ErrorEvent>{
this.emit('error', <ErrorEvent>{
message: err.message
});
}
}
),
fs.watch(`${locations.src()}/template.html`, () => {
fs.watch(`${src}/template.html`, () => {
this.dev_server.send({
action: 'reload'
});
@@ -186,12 +212,12 @@ class Watcher extends EventEmitter {
let deferred = new Deferred();
// TODO watch the configs themselves?
const compilers: Compilers = await create_compilers(this.bundler, this.dirs);
const compilers: Compilers = await create_compilers(this.bundler, cwd, src, dest, false);
let log = '';
const emitFatal = () => {
this.emit('fatal', <events.FatalEvent>{
this.emit('fatal', <FatalEvent>{
message: `Server crashed`,
log
});
@@ -215,7 +241,7 @@ class Watcher extends EventEmitter {
ports.wait(this.port)
.then((() => {
this.emit('ready', <events.ReadyEvent>{
this.emit('ready', <ReadyEvent>{
port: this.port,
process: this.proc
});
@@ -233,7 +259,7 @@ class Watcher extends EventEmitter {
.catch(err => {
if (this.crashed) return;
this.emit('fatal', <events.FatalEvent>{
this.emit('fatal', <FatalEvent>{
message: `Server is not listening on port ${this.port}`,
log
});
@@ -312,7 +338,9 @@ class Watcher extends EventEmitter {
create_serviceworker_manifest({
manifest_data,
client_files
output,
client_files,
static_files
});
deferred.fulfil();
@@ -361,7 +389,7 @@ class Watcher extends EventEmitter {
};
process.nextTick(() => {
this.emit('invalid', <events.InvalidEvent>{
this.emit('invalid', <InvalidEvent>{
changed: Array.from(this.current_build.changed),
invalid: {
server: this.current_build.rebuilding.has('server'),
@@ -384,7 +412,7 @@ class Watcher extends EventEmitter {
compiler.watch((err?: Error, result?: CompileResult) => {
if (err) {
this.emit('error', <events.ErrorEvent>{
this.emit('error', <ErrorEvent>{
type: name,
message: err.message
});
@@ -455,14 +483,12 @@ class DevServer {
}
}
function noop() {}
function watch_dir(
dir: string,
filter: ({ path, stats }: { path: string, stats: fs.Stats }) => boolean,
callback: () => void
) {
let watch;
let watch: any;
let closed = false;
import('cheap-watch').then(CheapWatch => {
@@ -470,7 +496,7 @@ function watch_dir(
watch = new CheapWatch({ dir, filter, debounce: 50 });
watch.on('+', ({ isNew }) => {
watch.on('+', ({ isNew }: { isNew: boolean }) => {
if (isNew) callback();
});

View File

@@ -4,58 +4,57 @@ import * as sander from 'sander';
import * as url from 'url';
import fetch from 'node-fetch';
import * as ports from 'port-authority';
import { EventEmitter } from 'events';
import clean_html from './utils/clean_html';
import minify_html from './utils/minify_html';
import Deferred from './utils/Deferred';
import * as events from './interfaces';
import { noop } from './utils/noop';
type Opts = {
build: string,
dest: string,
static: string,
build_dir?: string,
export_dir?: string,
cwd?: string,
static?: string,
basepath?: string,
timeout: number | false
timeout?: number | false,
oninfo?: ({ message }: { message: string }) => void;
onfile?: ({ file, size, status }: { file: string, size: number, status: number }) => void;
};
export function exporter(opts: Opts) {
const emitter = new EventEmitter();
execute(emitter, opts).then(
() => {
emitter.emit('done', <events.DoneEvent>{}); // TODO do we need to pass back any info?
},
error => {
emitter.emit('error', <events.ErrorEvent>{
error
});
}
);
return emitter;
}
function resolve(from: string, to: string) {
return url.parse(url.resolve(from, to));
}
type URL = url.UrlWithStringQuery;
async function execute(emitter: EventEmitter, opts: Opts) {
const export_dir = path.join(opts.dest, opts.basepath);
export { _export as export };
async function _export({
cwd,
static: static_files = 'static',
build_dir = '__sapper__/build',
export_dir = '__sapper__/export',
basepath = '',
timeout = 5000,
oninfo = noop,
onfile = noop
}: Opts = {}) {
cwd = path.resolve(cwd);
static_files = path.resolve(cwd, static_files);
build_dir = path.resolve(cwd, build_dir);
export_dir = path.resolve(cwd, export_dir, basepath);
// Prep output directory
sander.rimrafSync(export_dir);
sander.copydirSync(opts.static).to(export_dir);
sander.copydirSync(opts.build, 'client').to(export_dir, 'client');
sander.copydirSync(static_files).to(export_dir);
sander.copydirSync(build_dir, 'client').to(export_dir, 'client');
if (sander.existsSync(opts.build, 'service-worker.js')) {
sander.copyFileSync(opts.build, 'service-worker.js').to(export_dir, 'service-worker.js');
if (sander.existsSync(build_dir, 'service-worker.js')) {
sander.copyFileSync(build_dir, 'service-worker.js').to(export_dir, 'service-worker.js');
}
if (sander.existsSync(opts.build, 'service-worker.js.map')) {
sander.copyFileSync(opts.build, 'service-worker.js.map').to(export_dir, 'service-worker.js.map');
if (sander.existsSync(build_dir, 'service-worker.js.map')) {
sander.copyFileSync(build_dir, 'service-worker.js.map').to(export_dir, 'service-worker.js.map');
}
const port = await ports.find(3000);
@@ -64,19 +63,18 @@ async function execute(emitter: EventEmitter, opts: Opts) {
const host = `localhost:${port}`;
const origin = `${protocol}//${host}`;
const root = resolve(origin, opts.basepath || '');
const root = resolve(origin, basepath);
if (!root.href.endsWith('/')) root.href += '/';
emitter.emit('info', {
oninfo({
message: `Crawling ${root.href}`
});
const proc = child_process.fork(path.resolve(`${opts.build}/server/server.js`), [], {
cwd: process.cwd(),
const proc = child_process.fork(path.resolve(`${build_dir}/server/server.js`), [], {
cwd,
env: Object.assign({
PORT: port,
NODE_ENV: 'production',
SAPPER_DEST: opts.build,
SAPPER_EXPORT: 'true'
}, process.env)
});
@@ -98,7 +96,7 @@ async function execute(emitter: EventEmitter, opts: Opts) {
body = minify_html(body);
}
emitter.emit('file', <events.FileEvent>{
onfile({
file,
size: body.length,
status
@@ -119,9 +117,9 @@ async function execute(emitter: EventEmitter, opts: Opts) {
seen.add(pathname);
const timeout_deferred = new Deferred();
const timeout = setTimeout(() => {
const the_timeout = setTimeout(() => {
timeout_deferred.reject(new Error(`Timed out waiting for ${url.href}`));
}, opts.timeout);
}, timeout);
const r = await Promise.race([
fetch(url.href, {
@@ -130,7 +128,7 @@ async function execute(emitter: EventEmitter, opts: Opts) {
timeout_deferred.promise
]);
clearTimeout(timeout); // prevent it hanging at the end
clearTimeout(the_timeout); // prevent it hanging at the end
let type = r.headers.get('Content-Type');
let body = await r.text();
@@ -189,6 +187,6 @@ async function execute(emitter: EventEmitter, opts: Opts) {
}
function get_href(attrs: string) {
const match = /href\s*=\s*(?:"(.+?)"|'(.+?)'|([^\s>]+))/.exec(attrs);
const match = /href\s*=\s*(?:"(.*?)"|'(.+?)'|([^\s>]+))/.exec(attrs);
return match[1] || match[2] || match[3];
}

View File

@@ -1,7 +1,6 @@
import { locations } from '../config';
import { create_manifest_data } from '../core';
export function find_page(pathname: string, cwd = locations.routes()) {
export function find_page(pathname: string, cwd = 'src/routes') {
const { pages } = create_manifest_data(cwd);
for (let i = 0; i < pages.length; i += 1) {

View File

@@ -1,45 +0,0 @@
import * as child_process from 'child_process';
import { CompileResult } from '../core/create_compilers/interfaces';
export type ReadyEvent = {
port: number;
process: child_process.ChildProcess;
};
export type ErrorEvent = {
type: string;
message: string;
};
export type FatalEvent = {
message: string;
log?: string;
};
export type InvalidEvent = {
changed: string[];
invalid: {
client: boolean;
server: boolean;
serviceworker: boolean;
}
};
export type BuildEvent = {
type: string;
errors: Array<{ file: string, message: string, duplicate: boolean }>;
warnings: Array<{ file: string, message: string, duplicate: boolean }>;
duration: number;
result: CompileResult;
}
export type FileEvent = {
file: string;
size: number;
}
export type FailureEvent = {
}
export type DoneEvent = {};

View File

@@ -7,6 +7,7 @@ export default function minify_html(html: string) {
conservativeCollapse: true,
decodeEntities: true,
html5: true,
ignoreCustomComments: [/^#/],
minifyCSS: true,
minifyJS: false,
removeAttributeQuotes: true,

1
src/api/utils/noop.ts Normal file
View File

@@ -0,0 +1 @@
export function noop() {}

View File

@@ -2,11 +2,21 @@ import * as fs from 'fs';
import * as path from 'path';
import sade from 'sade';
import colors from 'kleur';
import prettyMs from 'pretty-ms';
import * as pkg from '../package.json';
import { elapsed, repeat, left_pad, format_milliseconds } from './utils';
import { InvalidEvent, ErrorEvent, FatalEvent, BuildEvent, ReadyEvent } from './interfaces';
const prog = sade('sapper').version(pkg.version);
if (process.argv[2] === 'start') {
// remove this in a future version
console.error(colors.bold.red(`'sapper start' has been removed`));
console.error(`Use 'node [build_dir]' instead`);
process.exit(1);
}
const start = Date.now();
prog.command('dev')
.describe('Start a development server')
.option('-p, --port', 'Specify a port')
@@ -15,16 +25,114 @@ prog.command('dev')
.option('--hot', 'Use hot module replacement (requires webpack)', true)
.option('--live', 'Reload on changes if not using --hot', true)
.option('--bundler', 'Specify a bundler (rollup or webpack)')
.option('--cwd', 'Current working directory', '.')
.option('--src', 'Source directory', 'src')
.option('--routes', 'Routes directory', 'src/routes')
.option('--static', 'Static files directory', 'static')
.option('--output', 'Sapper output directory', '__sapper__')
.option('--build-dir', 'Development build directory', '__sapper__/dev')
.action(async (opts: {
port: number,
open: boolean,
'dev-port': number,
live: boolean,
hot: boolean,
bundler?: string
bundler?: 'rollup' | 'webpack',
cwd: string,
src: string,
routes: string,
static: string,
output: string,
'build-dir': string
}) => {
const { dev } = await import('./cli/dev');
dev(opts);
const { dev } = await import('./api/dev');
try {
const watcher = dev({
cwd: opts.cwd,
src: opts.src,
routes: opts.routes,
static: opts.static,
output: opts.output,
dest: opts['build-dir'],
port: opts.port,
'dev-port': opts['dev-port'],
live: opts.live,
hot: opts.hot,
bundler: opts.bundler
});
let first = true;
watcher.on('ready', async (event: ReadyEvent) => {
if (first) {
console.log(colors.bold.cyan(`> Listening on http://localhost:${event.port}`));
if (opts.open) {
const { exec } = await import('child_process');
exec(`open http://localhost:${event.port}`);
}
first = false;
}
// TODO clear screen?
event.process.stdout.on('data', data => {
process.stdout.write(data);
});
event.process.stderr.on('data', data => {
process.stderr.write(data);
});
});
watcher.on('invalid', (event: InvalidEvent) => {
const changed = event.changed.map(filename => path.relative(process.cwd(), filename)).join(', ');
console.log(`\n${colors.bold.cyan(changed)} changed. rebuilding...`);
});
watcher.on('error', (event: ErrorEvent) => {
console.log(colors.red(`${event.type}`));
console.log(colors.red(event.message));
});
watcher.on('fatal', (event: FatalEvent) => {
console.log(colors.bold.red(`> ${event.message}`));
if (event.log) console.log(event.log);
});
watcher.on('build', (event: BuildEvent) => {
if (event.errors.length) {
console.log(colors.bold.red(`${event.type}`));
event.errors.filter(e => !e.duplicate).forEach(error => {
if (error.file) console.log(colors.bold(error.file));
console.log(error.message);
});
const hidden = event.errors.filter(e => e.duplicate).length;
if (hidden > 0) {
console.log(`${hidden} duplicate ${hidden === 1 ? 'error' : 'errors'} hidden\n`);
}
} else if (event.warnings.length) {
console.log(colors.bold.yellow(`${event.type}`));
event.warnings.filter(e => !e.duplicate).forEach(warning => {
if (warning.file) console.log(colors.bold(warning.file));
console.log(warning.message);
});
const hidden = event.warnings.filter(e => e.duplicate).length;
if (hidden > 0) {
console.log(`${hidden} duplicate ${hidden === 1 ? 'warning' : 'warnings'} hidden\n`);
}
} else {
console.log(`${colors.bold.green(`${event.type}`)} ${colors.gray(`(${format_milliseconds(event.duration)})`)}`);
}
});
} catch (err) {
console.log(colors.bold.red(`> ${err.message}`));
process.exit(1);
}
});
prog.command('build [dest]')
@@ -32,29 +140,30 @@ prog.command('build [dest]')
.option('-p, --port', 'Default of process.env.PORT', '3000')
.option('--bundler', 'Specify a bundler (rollup or webpack, blank for auto)')
.option('--legacy', 'Create separate legacy build')
.option('--cwd', 'Current working directory', '.')
.option('--src', 'Source directory', 'src')
.option('--routes', 'Routes directory', 'src/routes')
.option('--output', 'Sapper output directory', '__sapper__')
.example(`build custom-dir -p 4567`)
.action(async (dest = '__sapper__/build', opts: {
port: string,
legacy: boolean,
bundler?: string
bundler?: 'rollup' | 'webpack',
cwd: string,
src: string,
routes: string,
output: string
}) => {
console.log(`> Building...`);
process.env.NODE_ENV = process.env.NODE_ENV || 'production';
process.env.SAPPER_DEST = dest;
const start = Date.now();
try {
const { build } = await import('./cli/build');
await build(opts);
await _build(opts.bundler, opts.legacy, opts.cwd, opts.src, opts.routes, opts.output, dest);
const launcher = path.resolve(dest, 'index.js');
fs.writeFileSync(launcher, `
// generated by sapper build at ${new Date().toISOString()}
process.env.NODE_ENV = process.env.NODE_ENV || 'production';
process.env.SAPPER_DEST = __dirname;
process.env.PORT = process.env.PORT || ${opts.port || 3000};
console.log('Starting server on port ' + process.env.PORT);
@@ -68,46 +177,66 @@ prog.command('build [dest]')
}
});
prog.command('start [dir]')
.describe('Start your app')
.option('-p, --port', 'Specify a port')
.option('-o, --open', 'Open a browser window')
.action(async (dir = 'build', opts: { port: number, open: boolean }) => {
const { start } = await import('./cli/start');
start(dir, opts);
});
prog.command('export [dest]')
.describe('Export your app as static files (if possible)')
.option('--build', '(Re)build app before exporting', true)
.option('--build-dir', 'Specify a custom temporary build directory', '__sapper__/build')
.option('--basepath', 'Specify a base path')
.option('--timeout', 'Milliseconds to wait for a page (--no-timeout to disable)', 5000)
.option('--legacy', 'Create separate legacy build')
.option('--bundler', 'Specify a bundler (rollup or webpack, blank for auto)')
.option('--cwd', 'Current working directory', '.')
.option('--src', 'Source directory', 'src')
.option('--routes', 'Routes directory', 'src/routes')
.option('--static', 'Static files directory', 'static')
.option('--output', 'Sapper output directory', '__sapper__')
.option('--build-dir', 'Intermediate build directory', '__sapper__/build')
.action(async (dest = '__sapper__/export', opts: {
build: boolean,
legacy: boolean,
bundler?: string,
'build-dir': string,
bundler?: 'rollup' | 'webpack',
basepath?: string,
timeout: number | false
timeout: number | false,
cwd: string,
src: string,
routes: string,
static: string,
output: string,
'build-dir': string,
}) => {
process.env.NODE_ENV = 'production';
process.env.SAPPER_DEST = opts['build-dir'];
const start = Date.now();
try {
if (opts.build) {
console.log(`> Building...`);
const { build } = await import('./cli/build');
await build(opts);
await _build(opts.bundler, opts.legacy, opts.cwd, opts.src, opts.routes, opts.output, opts['build-dir']);
console.error(`\n> Built in ${elapsed(start)}`);
}
const { exporter } = await import('./cli/export');
await exporter(dest, opts);
const { export: _export } = await import('./api/export');
const { default: pb } = await import('pretty-bytes');
await _export({
cwd: opts.cwd,
static: opts.static,
build_dir: opts['build-dir'],
export_dir: dest,
basepath: opts.basepath,
timeout: opts.timeout,
oninfo: event => {
console.log(colors.bold.cyan(`> ${event.message}`));
},
onfile: event => {
const size_color = event.size > 150000 ? colors.bold.red : event.size > 50000 ? colors.bold.yellow : colors.bold.gray;
const size_label = size_color(left_pad(pb(event.size), 10));
const file_label = event.status === 200
? event.file
: colors.bold[event.status >= 400 ? 'red' : 'yellow'](`(${event.status}) ${event.file}`);
console.log(`${size_label} ${file_label}`);
}
});
console.error(`\n> Finished in ${elapsed(start)}. Type ${colors.bold.cyan(`npx serve ${dest}`)} to run the app.`);
} catch (err) {
console.error(colors.bold.red(`> ${err.message}`));
@@ -115,10 +244,44 @@ prog.command('export [dest]')
}
});
// TODO upgrade
prog.parse(process.argv);
function elapsed(start: number) {
return prettyMs(Date.now() - start);
}
async function _build(
bundler: 'rollup' | 'webpack',
legacy: boolean,
cwd: string,
src: string,
routes: string,
output: string,
dest: string
) {
const { build } = await import('./api/build');
await build({
bundler,
legacy,
cwd,
src,
routes,
dest,
oncompile: event => {
let banner = `built ${event.type}`;
let c = colors.cyan;
const { warnings } = event.result;
if (warnings.length > 0) {
banner += ` with ${warnings.length} ${warnings.length === 1 ? 'warning' : 'warnings'}`;
c = colors.yellow;
}
console.log();
console.log(c(`┌─${repeat('─', banner.length)}─┐`));
console.log(c(`${colors.bold(banner) }`));
console.log(c(`└─${repeat('─', banner.length)}─┘`));
console.log(event.result.print());
}
});
}

View File

@@ -1,56 +0,0 @@
import { build as _build } from '../api/build';
import colors from 'kleur';
import { locations } from '../config';
import validate_bundler from './utils/validate_bundler';
import { repeat } from '../utils';
export function build(opts: { bundler?: string, legacy?: boolean }) {
const bundler = validate_bundler(opts.bundler);
if (opts.legacy && bundler === 'webpack') {
throw new Error(`Legacy builds are not supported for projects using webpack`);
}
return new Promise((fulfil, reject) => {
try {
const emitter = _build({
legacy: opts.legacy,
bundler
}, {
dest: locations.dest(),
src: locations.src(),
routes: locations.routes(),
webpack: 'webpack',
rollup: 'rollup'
});
emitter.on('build', event => {
let banner = `built ${event.type}`;
let c = colors.cyan;
const { warnings } = event.result;
if (warnings.length > 0) {
banner += ` with ${warnings.length} ${warnings.length === 1 ? 'warning' : 'warnings'}`;
c = colors.yellow;
}
console.log();
console.log(c(`┌─${repeat('─', banner.length)}─┐`));
console.log(c(`${colors.bold(banner) }`));
console.log(c(`└─${repeat('─', banner.length)}─┘`));
console.log(event.result.print());
});
emitter.on('error', event => {
reject(event.error);
});
emitter.on('done', event => {
fulfil();
});
} catch (err) {
reject(err);
}
});
}

View File

@@ -1,80 +0,0 @@
import * as path from 'path';
import colors from 'kleur';
import * as child_process from 'child_process';
import prettyMs from 'pretty-ms';
import { dev as _dev } from '../api/dev';
import * as events from '../api/interfaces';
export function dev(opts: { port: number, open: boolean, bundler?: string }) {
try {
const watcher = _dev(opts);
let first = true;
watcher.on('ready', (event: events.ReadyEvent) => {
if (first) {
console.log(colors.bold.cyan(`> Listening on http://localhost:${event.port}`));
if (opts.open) child_process.exec(`open http://localhost:${event.port}`);
first = false;
}
// TODO clear screen?
event.process.stdout.on('data', data => {
process.stdout.write(data);
});
event.process.stderr.on('data', data => {
process.stderr.write(data);
});
});
watcher.on('invalid', (event: events.InvalidEvent) => {
const changed = event.changed.map(filename => path.relative(process.cwd(), filename)).join(', ');
console.log(`\n${colors.bold.cyan(changed)} changed. rebuilding...`);
});
watcher.on('error', (event: events.ErrorEvent) => {
console.log(colors.red(`${event.type}`));
console.log(colors.red(event.message));
});
watcher.on('fatal', (event: events.FatalEvent) => {
console.log(colors.bold.red(`> ${event.message}`));
if (event.log) console.log(event.log);
});
watcher.on('build', (event: events.BuildEvent) => {
if (event.errors.length) {
console.log(colors.bold.red(`${event.type}`));
event.errors.filter(e => !e.duplicate).forEach(error => {
if (error.file) console.log(colors.bold(error.file));
console.log(error.message);
});
const hidden = event.errors.filter(e => e.duplicate).length;
if (hidden > 0) {
console.log(`${hidden} duplicate ${hidden === 1 ? 'error' : 'errors'} hidden\n`);
}
} else if (event.warnings.length) {
console.log(colors.bold.yellow(`${event.type}`));
event.warnings.filter(e => !e.duplicate).forEach(warning => {
if (warning.file) console.log(colors.bold(warning.file));
console.log(warning.message);
});
const hidden = event.warnings.filter(e => e.duplicate).length;
if (hidden > 0) {
console.log(`${hidden} duplicate ${hidden === 1 ? 'warning' : 'warnings'} hidden\n`);
}
} else {
console.log(`${colors.bold.green(`${event.type}`)} ${colors.gray(`(${prettyMs(event.duration)})`)}`);
}
});
} catch (err) {
console.log(colors.bold.red(`> ${err.message}`));
process.exit(1);
}
}

View File

@@ -1,51 +0,0 @@
import { exporter as _exporter } from '../api/export';
import colors from 'kleur';
import pb from 'pretty-bytes';
import { locations } from '../config';
import { left_pad } from '../utils';
export function exporter(export_dir: string, {
basepath = '',
timeout
}: {
basepath: string,
timeout: number | false
}) {
return new Promise((fulfil, reject) => {
try {
const emitter = _exporter({
build: locations.dest(),
static: locations.static(),
dest: export_dir,
basepath,
timeout
});
emitter.on('file', event => {
const size_color = event.size > 150000 ? colors.bold.red : event.size > 50000 ? colors.bold.yellow : colors.bold.gray;
const size_label = size_color(left_pad(pb(event.size), 10));
const file_label = event.status === 200
? event.file
: colors.bold[event.status >= 400 ? 'red' : 'yellow'](`(${event.status}) ${event.file}`);
console.log(`${size_label} ${file_label}`);
});
emitter.on('info', event => {
console.log(colors.bold.cyan(`> ${event.message}`));
});
emitter.on('error', event => {
reject(event.error);
});
emitter.on('done', event => {
fulfil();
});
} catch (err) {
console.log(`${colors.bold.red(`> ${err.message}`)}`);
process.exit(1);
}
});
}

View File

@@ -1,39 +0,0 @@
import * as fs from 'fs';
import * as path from 'path';
import * as child_process from 'child_process';
import colors from 'kleur';
import * as ports from 'port-authority';
export async function start(dir: string, opts: { port: number, open: boolean }) {
let port = opts.port || +process.env.PORT;
const resolved = path.resolve(dir);
const server = path.resolve(dir, 'server.js');
if (!fs.existsSync(server)) {
console.log(`${colors.bold.red(`> ${dir}/server.js does not exist — type ${colors.bold.cyan(dir === 'build' ? `npx sapper build` : `npx sapper build ${dir}`)} to create it`)}`);
return;
}
if (port) {
if (!await ports.check(port)) {
console.log(`${colors.bold.red(`> Port ${port} is unavailable`)}`);
return;
}
} else {
port = await ports.find(3000);
}
child_process.fork(server, [], {
cwd: process.cwd(),
env: Object.assign({
NODE_ENV: 'production',
PORT: port,
SAPPER_DEST: dir
}, process.env)
});
await ports.wait(port);
console.log(`${colors.bold.cyan(`> Listening on http://localhost:${port}`)}`);
if (opts.open) child_process.exec(`open http://localhost:${port}`);
}

View File

@@ -1,53 +0,0 @@
import * as fs from 'fs';
import colors from 'kleur';
export default async function upgrade() {
const upgraded = [
await upgrade_sapper_main()
].filter(Boolean);
if (upgraded.length === 0) {
console.log(`No changes!`);
}
}
async function upgrade_sapper_main() {
const _2xx = read('templates/2xx.html');
const _4xx = read('templates/4xx.html');
const _5xx = read('templates/5xx.html');
const pattern = /<script src='\%sapper\.main\%'><\/script>/;
let replaced = false;
['2xx', '4xx', '5xx'].forEach(code => {
const file = `templates/${code}.html`
const template = read(file);
if (!template) return;
if (/\%sapper\.main\%/.test(template)) {
if (!pattern.test(template)) {
console.log(`${colors.red(`Could not replace %sapper.main% in ${file}`)}`);
} else {
write(file, template.replace(pattern, `%sapper.scripts%`));
console.log(`${colors.green(`Replaced %sapper.main% in ${file}`)}`);
replaced = true;
}
}
});
return replaced;
}
function read(file: string) {
try {
return fs.readFileSync(file, 'utf-8');
} catch (err) {
console.error(err);
return null;
}
}
function write(file: string, data: string) {
fs.writeFileSync(file, data);
}

View File

@@ -1,11 +0,0 @@
import * as path from 'path';
export const dev = () => process.env.NODE_ENV !== 'production';
export const locations = {
base: () => path.resolve(process.env.SAPPER_BASE || ''),
src: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_SRC || 'src'),
static: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_STATIC || 'static'),
routes: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_ROUTES || 'src/routes'),
dest: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_DEST || `__sapper__/${dev() ? 'dev' : 'build'}`)
};

7
src/config/env.ts Normal file
View File

@@ -0,0 +1,7 @@
export let dev: boolean;
export let src: string;
export let dest: string;
export const set_dev = (_: boolean) => dev = _;
export const set_src = (_: string) => src = _;
export const set_dest = (_: string) => dest = _;

View File

@@ -1,15 +1,15 @@
import { locations, dev } from './config';
import { dev, src, dest } from './env';
export default {
dev: dev(),
dev,
client: {
input: () => {
return `${locations.src()}/client.js`
return `${src}/client.js`
},
output: () => {
let dir = `${locations.dest()}/client`;
let dir = `${dest}/client`;
if (process.env.SAPPER_LEGACY_BUILD) dir += `/legacy`;
return {
@@ -17,7 +17,7 @@ export default {
entryFileNames: '[name].[hash].js',
chunkFileNames: '[name].[hash].js',
format: 'esm',
sourcemap: dev()
sourcemap: dev
};
}
},
@@ -25,27 +25,27 @@ export default {
server: {
input: () => {
return {
server: `${locations.src()}/server.js`
server: `${src}/server.js`
};
},
output: () => {
return {
dir: `${locations.dest()}/server`,
dir: `${dest}/server`,
format: 'cjs',
sourcemap: dev()
sourcemap: dev
};
}
},
serviceworker: {
input: () => {
return `${locations.src()}/service-worker.js`;
return `${src}/service-worker.js`;
},
output: () => {
return {
file: `${locations.dest()}/service-worker.js`,
file: `${dest}/service-worker.js`,
format: 'iife'
}
}

View File

@@ -1,18 +1,18 @@
import { locations, dev } from './config';
import { dev, src, dest } from './env';
export default {
dev: dev(),
dev,
client: {
entry: () => {
return {
main: `${locations.src()}/client`
main: `${src}/client`
};
},
output: () => {
return {
path: `${locations.dest()}/client`,
path: `${dest}/client`,
filename: '[hash]/[name].js',
chunkFilename: '[hash]/[name].[id].js',
publicPath: `client/`
@@ -23,13 +23,13 @@ export default {
server: {
entry: () => {
return {
server: `${locations.src()}/server`
server: `${src}/server`
};
},
output: () => {
return {
path: `${locations.dest()}/server`,
path: `${dest}/server`,
filename: '[name].js',
chunkFilename: '[hash]/[name].[id].js',
libraryTarget: 'commonjs2'
@@ -40,13 +40,13 @@ export default {
serviceworker: {
entry: () => {
return {
'service-worker': `${locations.src()}/service-worker`
'service-worker': `${src}/service-worker`
};
},
output: () => {
return {
path: locations.dest(),
path: dest,
filename: '[name].js',
chunkFilename: '[name].[id].[hash].js'
}

View File

@@ -135,10 +135,10 @@ export default class RollupCompiler {
});
}
static async load_config() {
if (!rollup) rollup = relative('rollup', process.cwd());
static async load_config(cwd: string) {
if (!rollup) rollup = relative('rollup', cwd);
const input = path.resolve('rollup.config.js');
const input = path.resolve(cwd, 'rollup.config.js');
const bundle = await rollup.rollup({
input,

View File

@@ -5,7 +5,7 @@ import RollupCompiler from './RollupCompiler';
import extract_css from './extract_css';
import { left_pad } from '../../utils';
import { CompileResult, BuildInfo, CompileError, Chunk, CssFile } from './interfaces';
import { ManifestData, Dirs, PageComponent } from '../../interfaces';
import { ManifestData, Dirs } from '../../interfaces';
export default class RollupResult implements CompileResult {
duration: number;

View File

@@ -3,8 +3,8 @@ import * as path from 'path';
import hash from 'string-hash';
import * as codec from 'sourcemap-codec';
import { PageComponent, Dirs } from '../../interfaces';
import { CompileResult } from './interfaces';
import { posixify } from '../utils'
import { CompileResult, Chunk } from './interfaces';
import { posixify } from '../../utils'
const inline_sourcemap_header = 'data:application/json;charset=utf-8;base64,';
@@ -46,6 +46,65 @@ type SourceMap = {
mappings: string;
};
function get_css_from_modules(modules: string[], css_map: Map<string, string>, dirs: Dirs) {
const parts: string[] = [];
const mappings: number[][][] = [];
const combined_map: SourceMap = {
version: 3,
file: null,
sources: [],
sourcesContent: [],
names: [],
mappings: null
};
modules.forEach(module => {
if (!/\.css$/.test(module)) return;
const css = css_map.get(module);
const { code, map } = extract_sourcemap(css, module);
parts.push(code);
if (map) {
const lines = codec.decode(map.mappings);
if (combined_map.sources.length > 0 || combined_map.names.length > 0) {
lines.forEach(line => {
line.forEach(segment => {
// adjust source index
segment[1] += combined_map.sources.length;
// adjust name index
if (segment[4]) segment[4] += combined_map.names.length;
});
});
}
combined_map.sources.push(...map.sources);
combined_map.sourcesContent.push(...map.sourcesContent);
combined_map.names.push(...map.names);
mappings.push(...lines);
}
});
if (parts.length > 0) {
combined_map.mappings = codec.encode(mappings);
combined_map.sources = combined_map.sources.map(source => path.relative(`${dirs.dest}/client`, source));
return {
code: parts.join('\n'),
map: combined_map
};
}
return null;
}
export default function extract_css(client_result: CompileResult, components: PageComponent[], dirs: Dirs) {
const result: {
main: string | null;
@@ -57,165 +116,113 @@ export default function extract_css(client_result: CompileResult, components: Pa
if (!client_result.css_files) return; // Rollup-only for now
const unaccounted_for = new Set();
let asset_dir = `${dirs.dest}/client`;
if (process.env.SAPPER_LEGACY_BUILD) asset_dir += '/legacy';
const css_map = new Map();
client_result.css_files.forEach(css => {
unaccounted_for.add(css.id);
css_map.set(css.id, css.code);
const unclaimed = new Set(client_result.css_files.map(x => x.id));
const lookup = new Map();
client_result.chunks.forEach(chunk => {
lookup.set(chunk.file, chunk);
});
const chunk_map = new Map();
client_result.chunks.forEach(chunk => {
chunk_map.set(chunk.file, chunk);
const css_map = new Map();
client_result.css_files.forEach(css_module => {
css_map.set(css_module.id, css_module.code);
});
const chunks_with_css = new Set();
// figure out which chunks belong to which components...
const component_owners = new Map();
// concatenate and emit CSS
client_result.chunks.forEach(chunk => {
chunk.modules.forEach(module => {
const component = posixify(path.relative(dirs.routes, module));
component_owners.set(component, chunk);
});
const css_modules = chunk.modules.filter(m => css_map.has(m));
if (!css_modules.length) return;
const css = get_css_from_modules(css_modules, css_map, dirs);
const { code, map } = css;
const output_file_name = chunk.file.replace(/\.js$/, '.css');
map.file = output_file_name;
map.sources = map.sources.map(source => path.relative(`${asset_dir}`, source));
fs.writeFileSync(`${asset_dir}/${output_file_name}`, `${code}\n/* sourceMappingURL=./${output_file_name}.map */`);
fs.writeFileSync(`${asset_dir}/${output_file_name}.map`, JSON.stringify(map, null, ' '));
chunks_with_css.add(chunk);
});
const chunks_depended_upon_by_component = new Map();
const entry = path.resolve(dirs.src, 'client.js');
const entry_chunk = client_result.chunks.find(chunk => chunk.modules.indexOf(entry) !== -1);
// ...so we can figure out which chunks don't belong
const entry_chunk_dependencies: Set<Chunk> = new Set([entry_chunk]);
const entry_css_modules: string[] = [];
// recursively find the chunks this component depends on
entry_chunk_dependencies.forEach(chunk => {
chunk.imports.forEach(file => {
entry_chunk_dependencies.add(lookup.get(file));
});
if (chunks_with_css.has(chunk)) {
chunk.modules.forEach(file => {
unclaimed.delete(file);
if (css_map.has(file)) {
entry_css_modules.push(file);
}
});
}
});
// figure out which (css-having) chunks each component depends on
components.forEach(component => {
const chunk = component_owners.get(component.file);
const resolved = path.resolve(dirs.routes, component.file);
const chunk: Chunk = client_result.chunks.find(chunk => chunk.modules.indexOf(resolved) !== -1);
if (!chunk) {
// this should never happen!
throw new Error(`Could not find chunk that owns ${component.file}`);
}
const chunks = new Set([chunk]);
chunks.forEach(chunk => {
chunk.imports.forEach((file: string) => {
const chunk = chunk_map.get(file);
if (chunk) chunks.add(chunk);
const chunk_dependencies: Set<Chunk> = new Set([chunk]);
const css_dependencies: string[] = [];
// recursively find the chunks this component depends on
chunk_dependencies.forEach(chunk => {
chunk.imports.forEach(file => {
chunk_dependencies.add(lookup.get(file));
});
});
chunks.forEach(chunk => {
chunk.modules.forEach((module: string) => {
unaccounted_for.delete(module);
});
});
if (chunks_with_css.has(chunk)) {
css_dependencies.push(chunk.file.replace(/\.js$/, '.css'));
chunks_depended_upon_by_component.set(
component,
chunks
);
});
function get_css_from_modules(modules: string[]) {
const parts: string[] = [];
const mappings: number[][][] = [];
const combined_map: SourceMap = {
version: 3,
file: null,
sources: [],
sourcesContent: [],
names: [],
mappings: null
};
modules.forEach(module => {
if (!/\.css$/.test(module)) return;
const css = css_map.get(module);
const { code, map } = extract_sourcemap(css, module);
parts.push(code);
if (map) {
const lines = codec.decode(map.mappings);
if (combined_map.sources.length > 0 || combined_map.names.length > 0) {
lines.forEach(line => {
line.forEach(segment => {
// adjust source index
segment[1] += combined_map.sources.length;
// adjust name index
if (segment[4]) segment[4] += combined_map.names.length;
});
});
}
combined_map.sources.push(...map.sources);
combined_map.sourcesContent.push(...map.sourcesContent);
combined_map.names.push(...map.names);
mappings.push(...lines);
chunk.modules.forEach(file => {
unclaimed.delete(file);
});
}
});
if (parts.length > 0) {
combined_map.mappings = codec.encode(mappings);
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 main = client_result.assets.main;
if (process.env.SAPPER_LEGACY_BUILD) main = `legacy/${main}`;
const entry = fs.readFileSync(`${dirs.dest}/client/${main}`, 'utf-8');
const replacements = new Map();
chunks_depended_upon_by_component.forEach((chunks, component) => {
const chunks_with_css = Array.from(chunks).filter(chunk => {
const css = get_css_from_modules(chunk.modules);
if (css) {
const { code, map } = css;
const output_file_name = chunk.file.replace(/\.js$/, '.css');
map.file = output_file_name;
map.sources = map.sources.map(source => path.relative(`${dirs.dest}/client`, source));
fs.writeFileSync(`${dirs.dest}/client/${output_file_name}`, `${code}\n/* sourceMappingURL=client/${output_file_name}.map */`);
fs.writeFileSync(`${dirs.dest}/client/${output_file_name}.map`, JSON.stringify(map, null, ' '));
return true;
}
});
const files = chunks_with_css.map(chunk => chunk.file.replace(/\.js$/, '.css'));
replacements.set(
component.file,
files
);
result.chunks[component.file] = files;
result.chunks[component.file] = css_dependencies;
});
fs.readdirSync(`${dirs.dest}/client`).forEach(file => {
const source = fs.readFileSync(`${dirs.dest}/client/${file}`, 'utf-8');
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));
return JSON.stringify(result.chunks[route]);
});
fs.writeFileSync(`${dirs.dest}/client/${file}`, replaced);
fs.writeFileSync(`${asset_dir}/${file}`, replaced);
});
const leftover = get_css_from_modules(Array.from(unaccounted_for));
unclaimed.forEach(file => {
entry_css_modules.push(file);
});
const leftover = get_css_from_modules(entry_css_modules, css_map, dirs);
if (leftover) {
const { code, map } = leftover;
@@ -224,10 +231,10 @@ export default function extract_css(client_result: CompileResult, components: Pa
const output_file_name = `main.${main_hash}.css`;
map.file = output_file_name;
map.sources = map.sources.map(source => path.relative(`${dirs.dest}/client`, source));
map.sources = map.sources.map(source => path.relative(asset_dir, source));
fs.writeFileSync(`${dirs.dest}/client/${output_file_name}`, `${code}\n/* sourceMappingURL=client/${output_file_name}.map */`);
fs.writeFileSync(`${dirs.dest}/client/${output_file_name}.map`, JSON.stringify(map, null, ' '));
fs.writeFileSync(`${asset_dir}/${output_file_name}`, `${code}\n/* sourceMappingURL=client/${output_file_name}.map */`);
fs.writeFileSync(`${asset_dir}/${output_file_name}.map`, JSON.stringify(map, null, ' '));
result.main = output_file_name;
}

View File

@@ -1,6 +1,7 @@
import * as path from 'path';
import RollupCompiler from './RollupCompiler';
import { WebpackCompiler } from './WebpackCompiler';
import { set_dev, set_src, set_dest } from '../../config/env';
export type Compiler = RollupCompiler | WebpackCompiler;
@@ -10,9 +11,19 @@ export type Compilers = {
serviceworker?: Compiler;
}
export default async function create_compilers(bundler: 'rollup' | 'webpack'): Promise<Compilers> {
export default async function create_compilers(
bundler: 'rollup' | 'webpack',
cwd: string,
src: string,
dest: string,
dev: boolean
): Promise<Compilers> {
set_dev(dev);
set_src(src);
set_dest(dest);
if (bundler === 'rollup') {
const config = await RollupCompiler.load_config();
const config = await RollupCompiler.load_config(cwd);
validate_config(config, 'rollup');
normalize_rollup_config(config.client);
@@ -30,7 +41,7 @@ export default async function create_compilers(bundler: 'rollup' | 'webpack'): P
}
if (bundler === 'webpack') {
const config = require(path.resolve('webpack.config.js'));
const config = require(path.resolve(cwd, 'webpack.config.js'));
validate_config(config, 'webpack');
return {

View File

@@ -1,10 +1,9 @@
import * as fs from 'fs';
import * as path from 'path';
import { locations } from '../config';
import { Page, PageComponent, ServerRoute, ManifestData } from '../interfaces';
import { posixify, reserved_words } from './utils';
import { posixify, reserved_words } from '../utils';
export default function create_manifest_data(cwd = locations.routes()): ManifestData {
export default function create_manifest_data(cwd: string): ManifestData {
// TODO remove in a future version
if (!fs.existsSync(cwd)) {
throw new Error(`As of Sapper 0.21, the routes/ directory should become src/routes/`);

View File

@@ -1,41 +1,56 @@
import * as fs from 'fs';
import * as path from 'path';
import glob from 'tiny-glob/sync.js';
import { posixify, stringify, write_if_changed } from './utils';
import { dev, locations } from '../config';
import { Page, PageComponent, ServerRoute, ManifestData } from '../interfaces';
import { posixify, stringify, walk, write_if_changed } from '../utils';
import { Page, PageComponent, ManifestData } from '../interfaces';
export function create_main_manifests({ bundler, manifest_data, dev_port }: {
export function create_main_manifests({
bundler,
manifest_data,
dev_port,
dev,
cwd,
src,
dest,
routes,
output
}: {
bundler: string,
manifest_data: ManifestData;
dev_port?: number;
dev: boolean;
cwd: string;
src: string;
dest: string;
routes: string;
output: string
}) {
const manifest_dir = '__sapper__';
if (!fs.existsSync(manifest_dir)) fs.mkdirSync(manifest_dir);
if (!fs.existsSync(output)) fs.mkdirSync(output);
const path_to_routes = path.relative(manifest_dir, locations.routes());
const path_to_routes = path.relative(output, routes);
const client_manifest = generate_client(manifest_data, path_to_routes, bundler, dev_port);
const server_manifest = generate_server(manifest_data, path_to_routes);
const client_manifest = generate_client(manifest_data, path_to_routes, bundler, dev, dev_port);
const server_manifest = generate_server(manifest_data, path_to_routes, cwd, src, dest, dev);
write_if_changed(
`${manifest_dir}/default-layout.html`,
`${output}/_layout.html`,
`<svelte:component this={child.component} {...child.props}/>`
);
write_if_changed(`${manifest_dir}/client.js`, client_manifest);
write_if_changed(`${manifest_dir}/server.js`, server_manifest);
write_if_changed(`${output}/client.js`, client_manifest);
write_if_changed(`${output}/server.js`, server_manifest);
}
export function create_serviceworker_manifest({ manifest_data, client_files }: {
export function create_serviceworker_manifest({ manifest_data, output, client_files, static_files }: {
manifest_data: ManifestData;
output: string;
client_files: string[];
static_files: string;
}) {
let files;
let files: string[];
// TODO remove in a future version
if (fs.existsSync(locations.static())) {
files = glob('**', { cwd: locations.static(), filesOnly: true });
if (fs.existsSync(static_files)) {
files = walk(static_files);
} else {
// TODO remove in a future version
if (fs.existsSync('assets')) {
throw new Error(`As of Sapper 0.21, the assets/ directory should become static/`);
}
@@ -55,13 +70,14 @@ export function create_serviceworker_manifest({ manifest_data, client_files }: {
export const routes = [\n\t${manifest_data.pages.map((r: Page) => `{ pattern: ${r.pattern} }`).join(',\n\t')}\n];
`.replace(/^\t\t/gm, '').trim();
write_if_changed(`__sapper__/service-worker.js`, code);
write_if_changed(`${output}/service-worker.js`, code);
}
function generate_client(
manifest_data: ManifestData,
path_to_routes: string,
bundler: string,
dev: boolean,
dev_port?: number
) {
const template_file = path.resolve(__dirname, '../templates/client.js');
@@ -120,7 +136,7 @@ function generate_client(
let footer = '';
if (dev()) {
if (dev) {
const sapper_dev_client = posixify(
path.resolve(__dirname, '../sapper-dev-client.js')
);
@@ -145,7 +161,11 @@ function generate_client(
function generate_server(
manifest_data: ManifestData,
path_to_routes: string
path_to_routes: string,
cwd: string,
src: string,
dest: string,
dev: boolean
) {
const template_file = path.resolve(__dirname, '../templates/server.js');
const template = fs.readFileSync(template_file, 'utf-8');
@@ -206,19 +226,19 @@ function generate_server(
error
};`.replace(/^\t\t/gm, '').trim();
const build_dir = path.relative(process.cwd(), locations.dest());
const src_dir = path.relative(process.cwd(), locations.src());
const build_dir = path.relative(cwd, dest);
const src_dir = path.relative(cwd, src);
return `// This file is generated by Sapper — do not edit it!\n` + template
.replace('__BUILD__DIR__', JSON.stringify(build_dir))
.replace('__SRC__DIR__', JSON.stringify(src_dir))
.replace('__DEV__', dev() ? 'true' : 'false')
.replace('__DEV__', dev ? 'true' : 'false')
.replace(/const manifest = __MANIFEST__;/, code);
}
function get_file(path_to_routes: string, component: PageComponent) {
if (component.default) {
return `./default-layout.html`;
return `./_layout.html`;
}
return posixify(`${path_to_routes}/${component.file}`);

View File

@@ -1,7 +1,6 @@
import * as fs from 'fs';
import { locations } from '../config';
export default function read_template(dir = locations.src()) {
export default function read_template(dir: string) {
try {
return fs.readFileSync(`${dir}/template.html`, 'utf-8');
} catch (err) {

View File

@@ -1,81 +0,0 @@
import * as fs from 'fs';
const previous_contents = new Map();
export function write_if_changed(file: string, code: string) {
if (code !== previous_contents.get(file)) {
previous_contents.set(file, code);
fs.writeFileSync(file, code);
fudge_mtime(file);
}
}
export function posixify(file: string) {
return file.replace(/[/\\]/g, '/');
}
export function stringify(string: string, includeQuotes: boolean = true) {
const quoted = JSON.stringify(string);
return includeQuotes ? quoted : quoted.slice(1, -1);
}
export function fudge_mtime(file: string) {
// need to fudge the mtime so that webpack doesn't go doolally
const { atime, mtime } = fs.statSync(file);
fs.utimesSync(
file,
new Date(atime.getTime() - 999999),
new Date(mtime.getTime() - 999999)
);
}
export const reserved_words = new Set([
'arguments',
'await',
'break',
'case',
'catch',
'class',
'const',
'continue',
'debugger',
'default',
'delete',
'do',
'else',
'enum',
'eval',
'export',
'extends',
'false',
'finally',
'for',
'function',
'if',
'implements',
'import',
'in',
'instanceof',
'interface',
'let',
'new',
'null',
'package',
'private',
'protected',
'public',
'return',
'static',
'super',
'switch',
'this',
'throw',
'true',
'try',
'typeof',
'var',
'void',
'while',
'with',
'yield',
]);

View File

@@ -1,3 +1,6 @@
import * as child_process from 'child_process';
import { CompileResult } from './core/create_compilers/interfaces';
export type Route = {
id: string;
handlers: {
@@ -44,9 +47,7 @@ export type ServerRoute = {
export type Dirs = {
dest: string,
src: string,
routes: string,
webpack: string,
rollup: string
routes: string
};
export type ManifestData = {
@@ -54,4 +55,47 @@ export type ManifestData = {
components: PageComponent[];
pages: Page[];
server_routes: ServerRoute[];
};
};
export type ReadyEvent = {
port: number;
process: child_process.ChildProcess;
};
export type ErrorEvent = {
type: string;
message: string;
};
export type FatalEvent = {
message: string;
log?: string;
};
export type InvalidEvent = {
changed: string[];
invalid: {
client: boolean;
server: boolean;
serviceworker: boolean;
}
};
export type BuildEvent = {
type: string;
errors: Array<{ file: string, message: string, duplicate: boolean }>;
warnings: Array<{ file: string, message: string, duplicate: boolean }>;
duration: number;
result: CompileResult;
};
export type FileEvent = {
file: string;
size: number;
};
export type FailureEvent = {
};
export type DoneEvent = {};

View File

@@ -1,3 +1,6 @@
import * as fs from 'fs';
import * as path from 'path';
export function left_pad(str: string, len: number) {
while (str.length < len) str = ` ${str}`;
return str;
@@ -7,4 +10,110 @@ export function repeat(str: string, i: number) {
let result = '';
while (i--) result += str;
return result;
}
}
export function format_milliseconds(ms: number) {
if (ms < 1000) return `${ms}ms`;
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
const minutes = ~~(ms / 60000);
const seconds = Math.round((ms % 60000) / 1000);
return `${minutes}m${seconds < 10 ? '0' : ''}${seconds}s`;
}
export function elapsed(start: number) {
return format_milliseconds(Date.now() - start);
}
export function walk(cwd: string, dir = cwd, files: string[] = []) {
fs.readdirSync(dir).forEach(file => {
const resolved = path.resolve(dir, file);
if (fs.statSync(resolved).isDirectory()) {
walk(cwd, resolved, files);
} else {
files.push(posixify(path.relative(cwd, resolved)));
}
});
return files;
}
export function posixify(str: string) {
return str.replace(/\\/g, '/');
}
const previous_contents = new Map();
export function write_if_changed(file: string, code: string) {
if (code !== previous_contents.get(file)) {
previous_contents.set(file, code);
fs.writeFileSync(file, code);
fudge_mtime(file);
}
}
export function stringify(string: string, includeQuotes: boolean = true) {
const quoted = JSON.stringify(string);
return includeQuotes ? quoted : quoted.slice(1, -1);
}
export function fudge_mtime(file: string) {
// need to fudge the mtime so that webpack doesn't go doolally
const { atime, mtime } = fs.statSync(file);
fs.utimesSync(
file,
new Date(atime.getTime() - 999999),
new Date(mtime.getTime() - 999999)
);
}
export const reserved_words = new Set([
'arguments',
'await',
'break',
'case',
'catch',
'class',
'const',
'continue',
'debugger',
'default',
'delete',
'do',
'else',
'enum',
'eval',
'export',
'extends',
'false',
'finally',
'for',
'function',
'if',
'implements',
'import',
'in',
'instanceof',
'interface',
'let',
'new',
'null',
'package',
'private',
'protected',
'public',
'return',
'static',
'super',
'switch',
'this',
'throw',
'true',
'try',
'typeof',
'var',
'void',
'while',
'with',
'yield',
]);

View File

@@ -90,8 +90,8 @@ export function select_route(url: URL): Target {
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, ' '));
const [, key, value] = /([^=]*)(?:=(.*))?/.exec(searchParam);
query[decodeURIComponent(key)] = decodeURIComponent((value || '').replace(/\+/g, ' '));
});
}
return { url, path, page, match, query };
@@ -106,16 +106,19 @@ export function scroll_state() {
};
}
export function navigate(target: Target, id: number): Promise<any> {
export function navigate(target: Target, id: number, noscroll?: boolean, hash?: string): Promise<any> {
let scroll: ScrollPosition;
if (id) {
// popstate or initial navigation
cid = id;
} else {
const current_scroll = scroll_state();
// clicked on a link. preserve scroll state
scroll_history[cid] = scroll_state();
scroll_history[cid] = current_scroll;
id = cid = ++uid;
scroll_history[cid] = { x: 0, y: 0 };
scroll_history[cid] = noscroll ? current_scroll : { x: 0, y: 0 };
}
cid = id;
@@ -135,13 +138,12 @@ export function navigate(target: Target, id: number): Promise<any> {
if (redirect) {
return goto(redirect.location, { replaceState: true });
}
render(data, nullable_depth, scroll_history[id], token);
render(data, nullable_depth, scroll_history[id], noscroll, hash, token);
if (document.activeElement) document.activeElement.blur();
});
}
function render(data: any, nullable_depth: number, scroll: ScrollPosition, token: {}) {
function render(data: any, nullable_depth: number, scroll: ScrollPosition, noscroll: boolean, hash: string, token: {}) {
if (current_token !== token) return;
if (root_component) {
@@ -180,8 +182,20 @@ function render(data: any, nullable_depth: number, scroll: ScrollPosition, token
});
}
if (scroll) {
scrollTo(scroll.x, scroll.y);
if (!noscroll) {
if (hash) {
// scroll is an element id (from a hash), we need to compute y.
const deep_linked = document.querySelector(hash);
if (deep_linked) {
scroll = {
x: 0,
y: deep_linked.getBoundingClientRect().top
};
}
}
scroll_history[cid] = scroll;
if (scroll) scrollTo(scroll.x, scroll.y);
}
Object.assign(root_props, data);
@@ -296,7 +310,7 @@ export function prepare_page(target: Target): Promise<{
};
}
const props = { path, query };
const props = { path, query, error: null, status: null };
const data = {
path,
preloading: false,
@@ -340,7 +354,7 @@ export function prepare_page(target: Target): Promise<{
}
function load_css(chunk: string) {
const href = `${initial_data.baseUrl}client/${chunk}`;
const href = `client/${chunk}`;
if (document.querySelector(`link[href="${href}"]`)) return;
return new Promise((fulfil, reject) => {

View File

@@ -4,7 +4,11 @@ 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));
if (target) {
if (!prefetching || href !== prefetching.href) {
set_prefetching(href, prepare_page(target));
}
return prefetching.promise;
}
}

View File

@@ -13,7 +13,7 @@ import {
set_cid
} from '../app';
import prefetch from '../prefetch/index';
import { Store } from '../types';
import { Store, ScrollPosition } from '../types';
export default function start(opts: {
target: Node,
@@ -36,16 +36,11 @@ export default function start(opts: {
return Promise.resolve().then(() => {
const { hash, href } = location;
const deep_linked = hash && document.getElementById(hash.slice(1));
scroll_history[uid] = deep_linked ?
{ x: 0, y: deep_linked.getBoundingClientRect().top } :
scroll_state();
history.replaceState({ id: uid }, '', href);
if (!initial_data.error) {
const target = select_route(new URL(location.href));
if (target) return navigate(target, uid);
if (target) return navigate(target, uid, false, hash);
}
});
}
@@ -84,7 +79,7 @@ function handle_click(event: MouseEvent) {
const href = String(svg ? (<SVGAElement>a).href.baseVal : a.href);
if (href === location.href) {
event.preventDefault();
if (!location.hash) event.preventDefault();
return;
}
@@ -103,7 +98,8 @@ function handle_click(event: MouseEvent) {
const target = select_route(url);
if (target) {
navigate(target, null);
const noscroll = a.hasAttribute('sapper-noscroll');
navigate(target, null, noscroll, url.hash);
event.preventDefault();
history.pushState({ id: cid }, '', url.href);
}

View File

@@ -33,7 +33,7 @@ export function get_page_handler(
}, req, res, statusCode, error || new Error('Unknown error in preload function'));
}
function handle_page(page: Page, req: Req, res: Res, status = 200, error: Error | string = null) {
async function handle_page(page: Page, req: Req, res: Res, status = 200, error: Error | string = null) {
const build_info: {
bundler: 'rollup' | 'webpack',
shimport: string | null,
@@ -56,12 +56,22 @@ export function get_page_handler(
});
}
const link = preloaded_chunks
.filter(file => file && !file.match(/\.map$/))
.map(file => `<${req.baseUrl}/client/${file}>;rel="preload";as="script"`)
.join(', ');
if (build_info.bundler === 'rollup') {
// TODO add dependencies and CSS
const link = preloaded_chunks
.filter(file => file && !file.match(/\.map$/))
.map(file => `<${req.baseUrl}/client/${file}>;rel="modulepreload"`)
.join(', ');
res.setHeader('Link', link);
res.setHeader('Link', link);
} else {
const link = preloaded_chunks
.filter(file => file && !file.match(/\.map$/))
.map(file => `<${req.baseUrl}/client/${file}>;rel="preload";as="script"`)
.join(', ');
res.setHeader('Link', link);
}
const store = store_getter ? store_getter(req, res) : null;
@@ -118,30 +128,37 @@ export function get_page_handler(
store
};
const root_preloaded = manifest.root.preload
? manifest.root.preload.call(preload_context, {
path: req.path,
query: req.query,
params: {}
})
: {};
let preloaded;
let match;
const match = error ? null : page.pattern.exec(req.path);
Promise.all([root_preloaded].concat(page.parts.map(part => {
if (!part) return null;
return part.component.preload
? part.component.preload.call(preload_context, {
try {
const root_preloaded = manifest.root.preload
? manifest.root.preload.call(preload_context, {
path: req.path,
query: req.query,
params: part.params ? part.params(match) : {}
params: {}
})
: {};
}))).catch(err => {
match = error ? null : page.pattern.exec(req.path);
preloaded = await 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 => {
preloaded = []; // appease TypeScript
}
try {
if (redirect) {
const location = `${req.baseUrl}/${redirect.location}`;
@@ -226,7 +243,7 @@ export function get_page_handler(
const legacy_main = `${req.baseUrl}/client/legacy/${build_info.legacy_assets.main}`;
script += `(function(){try{eval("async function x(){}");var main="${main}"}catch(e){main="${legacy_main}"};var s=document.createElement("script");try{new Function("if(0)import('')")();s.src=main;s.type="module";s.crossOrigin="use-credentials";}catch(e){s.src="${req.baseUrl}/client/shimport@${build_info.shimport}.js";s.setAttribute("data-main",main);}document.head.appendChild(s);}());`;
} else {
script += `var s=document.createElement("script");try{new Function("if(0)import('')")();s.src="${main}";s.type="module";s.crossOrigin="use-credentials";}catch(e){console.error(e);s.src="${req.baseUrl}/client/shimport@${build_info.shimport}.js";s.setAttribute("data-main","${main}")}document.head.appendChild(s)`;
script += `var s=document.createElement("script");try{new Function("if(0)import('')")();s.src="${main}";s.type="module";s.crossOrigin="use-credentials";}catch(e){s.src="${req.baseUrl}/client/shimport@${build_info.shimport}.js";s.setAttribute("data-main","${main}")}document.head.appendChild(s)`;
}
} else {
script += `</script><script src="${main}">`;
@@ -269,7 +286,7 @@ export function get_page_handler(
res.statusCode = status;
res.end(body);
}).catch(err => {
} catch(err) {
if (error) {
// we encountered an error while rendering the error page — oops
res.statusCode = 500;
@@ -277,7 +294,7 @@ export function get_page_handler(
} else {
handle_error(req, res, 500, err);
}
});
}
}
return function find_route(req: Req, res: Res, next: () => void) {

View File

@@ -2,7 +2,7 @@ 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) {
async function handle_route(route: ServerRoute, req: Req, res: Res, next: () => void) {
req.params = route.params(route.pattern.exec(req.path));
const method = req.method.toLowerCase();
@@ -53,7 +53,7 @@ export function get_server_route_handler(routes: ServerRoute[]) {
};
try {
handle_method(req, res, handle_next);
await handle_method(req, res, handle_next);
} catch (err) {
handle_next(err);
}

7
test/app/.gitignore vendored
View File

@@ -1,7 +0,0 @@
.DS_Store
node_modules
.sapper
yarn.lock
cypress/screenshots
templates/.*
dist

View File

@@ -1,81 +0,0 @@
# sapper-template
The default [Sapper](https://github.com/sveltejs/sapper) template. To clone it and get started:
```bash
npx degit sveltejs/sapper-template my-app
cd my-app
npm install # or yarn!
npm run dev
```
Open up [localhost:3000](http://localhost:3000) and start clicking around.
## Structure
Sapper expects to find three directories in the root of your project — `assets`, `routes` and `templates`.
### assets
The [assets](assets) directory contains any static assets that should be available. These are served using [serve-static](https://github.com/expressjs/serve-static).
In your [service-worker.js](templates/service-worker.js) file, Sapper makes these files available as `__assets__` so that you can cache them (though you can choose not to, for example if you don't want to cache very large files).
### routes
This is the heart of your Sapper app. There are two kinds of routes — *pages*, and *server routes*.
**Pages** are Svelte components written in `.html` files. When a user first visits the application, they will be served a server-rendered version of the route in question, plus some JavaScript that 'hydrates' the page and initialises a client-side router. From that point forward, navigating to other pages is handled entirely on the client for a fast, app-like feel. (Sapper will preload and cache the code for these subsequent pages, so that navigation is instantaneous.)
**Server routes** are modules written in `.js` files, that export functions corresponding to HTTP methods. Each function receives Express `request` and `response` objects as arguments, plus a `next` function. This is useful for creating a JSON API, for example.
There are three simple rules for naming the files that define your routes:
* A file called `routes/about.html` corresponds to the `/about` route. A file called `routes/blog/[slug].html` corresponds to the `/blog/:slug` route, in which case `params.slug` is available to the route
* The file `routes/index.html` (or `routes/index.js`) corresponds to the root of your app. `routes/about/index.html` is treated the same as `routes/about.html`.
* Files and directories with a leading underscore do *not* create routes. This allows you to colocate helper modules and components with the routes that depend on them — for example you could have a file called `routes/_helpers/datetime.js` and it would *not* create a `/_helpers/datetime` route
### templates
This directory should contain the following files at a minimum:
* [2xx.html](templates/2xx.html) — a template for the page to serve for valid requests
* [4xx.html](templates/4xx.html) — a template for 4xx-range errors (such as 404 Not Found)
* [5xx.html](templates/5xx.html) — a template for 5xx-range errors (such as 500 Internal Server Error)
* [main.js](templates/main.js) — this module initialises Sapper
* [service-worker.js](templates/service-worker.js) — your app's service worker
Inside the HTML templates, Sapper will inject various values as indicated by `%sapper.xxxx%` tags. Inside JavaScript files, Sapper will replace strings like `__dev__` with the appropriate value.
In lieu of documentation (bear with us), consult the files to see what variables are available and how they're used.
## Webpack config
Sapper uses webpack to provide code-splitting, dynamic imports and hot module reloading, as well as compiling your Svelte components. As long as you don't do anything daft, you can edit the configuration files to add whatever loaders and plugins you'd like.
## Production mode and deployment
To start a production version of your app, run `npm start`. This will disable hot module replacement, and activate the appropriate webpack plugins.
You can deploy your application to any environment that supports Node 8 or above. As an example, to deploy to [Now](https://zeit.co/now), run these commands:
```bash
npm install -g now
now
```
## Bugs and feedback
Sapper is in early development, and may have the odd rough edge here and there. Please be vocal over on the [Sapper issue tracker](https://github.com/sveltejs/sapper/issues).
## License
[LIL](LICENSE)

View File

@@ -1,12 +0,0 @@
import { Store } from 'svelte/store.js';
import * as sapper from '../__sapper__/client.js';
window.init = () => {
return sapper.start({
target: document.querySelector('#sapper'),
store: data => new Store(data)
});
};
window.prefetchRoutes = sapper.prefetchRoutes;
window.goto = sapper.goto;

View File

@@ -1,6 +0,0 @@
<svelte:head>
<title>{status}</title>
</svelte:head>
<h1>{status}</h1>
<p>{error.message}</p>

View File

@@ -1,19 +0,0 @@
<svelte:head>
<title>About</title>
</svelte:head>
<h1>About this site</h1>
<p>This is the 'about' page. There's not much here.</p>
<button class='prefetch' on:click='prefetch("blog/why-the-name")'>Why the name?</button>
<script>
import { prefetch } from '../../__sapper__/client.js';
export default {
methods: {
prefetch
}
};
</script>

View File

@@ -1,36 +0,0 @@
<svelte:head>
<title>{post.title}</title>
</svelte:head>
<h1>{post.title}</h1>
<div class='content'>
{@html post.html}
</div>
<script>
export default {
preload({ params, query }) {
// the `slug` parameter is available because this file
// is called [slug].html
const { slug } = params;
if (slug === 'throw-an-error') {
return this.error(500, 'something went wrong');
}
return fetch(`blog/${slug}.json`).then(r => {
if (r.status === 200) {
return r.json().then(post => ({ post }));
this.error(r.status, '')
}
if (r.status === 404) {
this.error(404, 'Not found');
} else {
throw new Error('Something went wrong');
}
});
}
};
</script>

View File

@@ -1,23 +0,0 @@
import posts from './_posts.js';
const lookup = {};
posts.forEach(post => {
lookup[post.slug] = JSON.stringify(post);
});
export function get(req, res, next) {
// the `slug` parameter is available because this file
// is called [slug].js
const { slug } = req.params;
if (slug in lookup) {
res.writeHead(200, {
'Content-Type': 'application/json',
'Cache-Control': `no-cache`
});
res.end(lookup[slug]);
} else {
next();
}
}

View File

@@ -1,124 +0,0 @@
// Ordinarily, you'd generate this data from markdown files in your
// repo, or fetch them from a database of some kind. But in order to
// avoid unnecessary dependencies in the starter template, and in the
// service of obviousness, we're just going to leave it here.
// This file is called `_posts.js` rather than `posts.js`, because
// we don't want to create an `/api/blog/posts` route — the leading
// underscore tells Sapper not to do that.
const posts = [
{
title: 'What is Sapper?',
slug: 'what-is-sapper',
html: `
<p>First, you have to know what <a href='https://svelte.technology'>Svelte</a> is. Svelte is a UI framework with a bold new idea: rather than providing a library that you write code with (like React or Vue, for example), it's a compiler that turns your components into highly optimized vanilla JavaScript. If you haven't already read the <a href='https://svelte.technology/blog/frameworks-without-the-framework'>introductory blog post</a>, you should!</p>
<p>Sapper is a Next.js-style framework (<a href='blog/how-is-sapper-different-from-next'>more on that here</a>) built around Svelte. It makes it embarrassingly easy to create extremely high performance web apps. Out of the box, you get:</p>
<ul>
<li>Code-splitting, dynamic imports and hot module replacement, powered by webpack</li>
<li>Server-side rendering (SSR) with client-side hydration</li>
<li>Service worker for offline support, and all the PWA bells and whistles</li>
<li>The nicest development experience you've ever had, or your money back</li>
</ul>
<p>It's implemented as Express middleware. Everything is set up and waiting for you to get started, but you keep complete control over the server, service worker, webpack config and everything else, so it's as flexible as you need it to be.</p>
`
},
{
title: 'How to use Sapper',
slug: 'how-to-use-sapper',
html: `
<h2>Step one</h2>
<p>Create a new project, using <a href='https://github.com/Rich-Harris/degit'>degit</a>:</p>
<pre><code>npx degit sveltejs/sapper-template my-app
cd my-app
npm install # or yarn!
npm run dev
</code></pre>
<h2>Step two</h2>
<p>Go to <a href='http://localhost:3000'>localhost:3000</a>. Open <code>my-app</code> in your editor. Edit the files in the <code>routes</code> directory or add new ones.</p>
<h2>Step three</h2>
<p>...</p>
<h2>Step four</h2>
<p>Resist overdone joke formats.</p>
`
},
{
title: 'Why the name?',
slug: 'why-the-name',
html: `
<p>In war, the soldiers who build bridges, repair roads, clear minefields and conduct demolitions — all under combat conditions — are known as <em>sappers</em>.</p>
<p>For web developers, the stakes are generally lower than those for combat engineers. But we face our own hostile environment: underpowered devices, poor network connections, and the complexity inherent in front-end engineering. Sapper, which is short for <strong>S</strong>velte <strong>app</strong> mak<strong>er</strong>, is your courageous and dutiful ally.</p>
`
},
{
title: 'How is Sapper different from Next.js?',
slug: 'how-is-sapper-different-from-next',
html: `
<p><a href='https://github.com/zeit/next.js/'>Next.js</a> is a React framework from <a href='https://zeit.co'>Zeit</a>, and is the inspiration for Sapper. There are a few notable differences, however:</p>
<ul>
<li>It's powered by <a href='https://svelte.technology'>Svelte</a> instead of React, so it's faster and your apps are smaller</li>
<li>Instead of route masking, we encode route parameters in filenames. For example, the page you're looking at right now is <code>routes/blog/[slug].html</code></li>
<li>As well as pages (Svelte components, which render on server or client), you can create <em>server routes</em> in your <code>routes</code> directory. These are just <code>.js</code> files that export functions corresponding to HTTP methods, and receive Express <code>request</code> and <code>response</code> objects as arguments. This makes it very easy to, for example, add a JSON API such as the one <a href='blog/how-is-sapper-different-from-next.json'>powering this very page</a></li>
<li>Links are just <code>&lt;a&gt;</code> elements, rather than framework-specific <code>&lt;Link&gt;</code> components. That means, for example, that <a href='blog/how-can-i-get-involved'>this link right here</a>, despite being inside a blob of HTML, works with the router as you'd expect.</li>
</ul>
`
},
{
title: 'How can I get involved?',
slug: 'how-can-i-get-involved',
html: `
<p>We're so glad you asked! Come on over to the <a href='https://github.com/sveltejs/svelte'>Svelte</a> and <a href='https://github.com/sveltejs/sapper'>Sapper</a> repos, and join us in the <a href='https://gitter.im/sveltejs/svelte'>Gitter chatroom</a>. Everyone is welcome, especially you!</p>
`
},
{
title: 'A very long post with deep links',
slug: 'a-very-long-post',
html: `
<h2 id='one'>One</h2>
<p>I'll have a vodka rocks. (Mom, it's breakfast time.) And a piece of toast. Let me out that Queen. Fried cheese… with club sauce.</p>
<p>Her lawyers are claiming the seal is worth $250,000. And that's not even including Buster's Swatch. This was a big get for God. What, so the guy we are meeting with can't even grow his own hair? COME ON! She's always got to wedge herself in the middle of us so that she can control everything. Yeah. Mom's awesome. It's, like, Hey, you want to go down to the whirlpool? Yeah, I don't have a husband. I call it Swing City. The CIA should've just Googled for his hideout, evidently. There are dozens of us! DOZENS! Yeah, like I'm going to take a whiz through this $5,000 suit. COME ON.</p>
<h2 id='two'>Two</h2>
<p>Tobias Fünke costume. Heart attack never stopped old big bear.</p>
<p>Nellie is blowing them all AWAY. I will be a bigger and hairier mole than the one on your inner left thigh! I'll sacrifice anything for my children.</p>
<p>Up yours, granny! You couldn't handle it! Hey, Dad. Look at you. You're a year older…and a year closer to death. Buster: Oh yeah, I guess that's kind of funny. Bob Loblaw Law Blog. The guy runs a prison, he can have any piece of ass he wants.</p>
<h2 id='three'>Three</h2>
<p>I prematurely shot my wad on what was supposed to be a dry run, so now I'm afraid I have something of a mess on my hands. Dead Dove DO NOT EAT. Never once touched my per diem. I'd go to Craft Service, get some raw veggies, bacon, Cup-A-Soup…baby, I got a stew goin'. You're losing blood, aren't you? Gob: Probably, my socks are wet. Sure, let the little fruit do it. HUZZAH! Although George Michael had only got to second base, he'd gone in head first, like Pete Rose. I will pack your sweet pink mouth with so much ice cream you'll be the envy of every Jerry and Jane on the block!</p>
<p>Gosh Mom… after all these years, God's not going to take a call from you. Come on, this is a Bluth family celebration. It's no place for children.</p>
<p>And I wouldn't just lie there, if that's what you're thinking. That's not what I WAS thinking. Who? i just dont want him to point out my cracker ass in front of ann. When a man needs to prove to a woman that he's actually… When a man loves a woman… Heyyyyyy Uncle Father Oscar. [Stabbing Gob] White power! Gob: I'm white! Let me take off my assistant's skirt and put on my Barbra-Streisand-in-The-Prince-of-Tides ass-masking therapist pantsuit. In the mid '90s, Tobias formed a folk music band with Lindsay and Maebe which he called Dr. Funke's 100 Percent Natural Good Time Family Band Solution. The group was underwritten by the Natural Food Life Company, a division of Chem-Grow, an Allen Crayne acqusition, which was part of the Squimm Group. Their motto was simple: We keep you alive.</p>
<h2 id='four'>Four</h2>
<p>If you didn't have adult onset diabetes, I wouldn't mind giving you a little sugar. Everybody dance NOW. And the soup of the day is bread. Great, now I'm gonna smell to high heaven like a tuna melt!</p>
<p>That's how Tony Wonder lost a nut. She calls it a Mayonegg. Go ahead, touch the Cornballer. There's a new daddy in town. A discipline daddy.</p>
`
},
{
title: 'Encödïng test',
slug: 'encödïng-test',
html: `
<p>It works</p>
`
}
];
posts.forEach(post => {
post.html = post.html.replace(/^\t{3}/gm, '');
});
export default posts;

View File

@@ -1,25 +0,0 @@
<svelte:head>
<title>Blog</title>
</svelte:head>
<h1>Recent posts</h1>
<ul>
{#each posts as post}
<!-- we're using the non-standard `rel=prefetch` attribute to
tell Sapper to load the data for the page as soon as
the user hovers over the link or taps it, instead of
waiting for the 'click' event -->
<li><a rel='prefetch' href='blog/{post.slug}'>{post.title}</a></li>
{/each}
</ul>
<script>
export default {
preload({ params, query }) {
return fetch(`blog.json`).then(r => r.json()).then(posts => {
return { posts };
});
}
};
</script>

View File

@@ -1,17 +0,0 @@
import posts from './_posts.js';
const contents = JSON.stringify(posts.map(post => {
return {
title: post.title,
slug: post.slug
};
}));
export function get(req, res) {
res.writeHead(200, {
'Content-Type': 'application/json',
'Cache-Control': `max-age=${30 * 60 * 1e3}` // cache for 30 minutes
});
res.end(contents);
}

View File

@@ -1,12 +0,0 @@
<h1>{slug} ({message})</h1>
<script>
export default {
preload({ params, query }) {
return {
slug: params.slug,
message: query.message
};
}
};
</script>

View File

@@ -1,31 +0,0 @@
<svelte:head>
<title>Sapper project template</title>
</svelte:head>
<h1>Great success!</h1>
<a href='.'>home</a>
<a href='about'>about</a>
<a href='slow-preload'>slow preload</a>
<a href='non-sapper-redirect-from'>redirect</a>
<a href='redirect-from'>redirect</a>
<a href='redirect-root'>redirect (root)</a>
<a href='blog/nope'>broken link</a>
<a href='blog/throw-an-error'>error link</a>
<a href='credentials?creds=include'>credentials</a>
<a rel=prefetch class='{page === "blog" ? "selected" : ""}' href='blog'>blog</a>
<a href="const">const</a>
<a href="echo/page/encöded?message=hëllö+wörld">echo/page/encöded?message=hëllö+wörld</a>
<a href="echo/page/empty?message">echo/page/empty?message</a>
<div class='hydrate-test'></div>
<style>
h1 {
text-align: center;
font-size: 2.8em;
text-transform: uppercase;
font-weight: 700;
margin: 0 0 0.5em 0;
}
</style>

View File

@@ -1 +0,0 @@
<h1>it works</h1>

View File

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

View File

@@ -1,3 +0,0 @@
export function get() {
throw new Error('nope');
}

View File

@@ -1,126 +0,0 @@
import fs from 'fs';
import { resolve } from 'url';
import express from 'express';
import serve from 'serve-static';
import { Store } from 'svelte/store.js';
import * as sapper from '../__sapper__/server.js';
let pending;
let ended;
process.on('message', message => {
if (message.action === 'start') {
if (pending) {
throw new Error(`Already capturing`);
}
pending = new Set();
ended = false;
process.send({ type: 'ready' });
}
if (message.action === 'end') {
ended = true;
if (pending.size === 0) {
process.send({ type: 'done' });
pending = null;
}
}
});
const app = express();
const { PORT = 3000, BASEPATH = '' } = process.env;
const base = `http://localhost:${PORT}${BASEPATH}/`;
// this allows us to do e.g. `fetch('/api/blog')` on the server
const fetch = require('node-fetch');
global.fetch = (url, opts) => {
return fetch(resolve(base, url), opts);
};
const middlewares = [
serve('assets'),
// set test cookie
(req, res, next) => {
res.setHeader('Set-Cookie', ['a=1; Path=/', 'b=2; Path=/']);
next();
},
// emit messages so we can capture requests
(req, res, next) => {
if (!pending) return next();
pending.add(req.url);
const { write, end } = res;
const chunks = [];
res.write = function(chunk) {
chunks.push(new Buffer(chunk));
write.apply(res, arguments);
};
res.end = function(chunk) {
if (chunk) chunks.push(new Buffer(chunk));
end.apply(res, arguments);
if (pending) pending.delete(req.url);
process.send({
method: req.method,
url: req.url,
status: res.statusCode,
headers: res._headers,
body: Buffer.concat(chunks).toString()
});
if (pending && pending.size === 0 && ended) {
process.send({ type: 'done' });
}
};
next();
},
// set up some values for the store
(req, res, next) => {
req.hello = 'hello';
res.locals = { name: 'world' };
next();
},
sapper.middleware({
store: (req, res) => {
return new Store({
title: `${req.hello} ${res.locals.name}`
});
},
ignore: [
/foobar/i,
'/buzz',
'fizz',
x => x === '/hello'
]
}),
];
app.get(`${BASEPATH}/non-sapper-redirect-from`, (req, res) => {
res.writeHead(301, {
Location: `${BASEPATH}/non-sapper-redirect-to`
});
res.end();
});
if (BASEPATH) {
app.use(BASEPATH, ...middlewares);
} else {
app.use(...middlewares);
}
['foobar', 'buzz', 'fizzer', 'hello'].forEach(uri => {
app.get('/'+uri, (req, res) => res.end(uri));
});
app.listen(PORT);

View File

@@ -1,33 +0,0 @@
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width'>
<meta name='theme-color' content='#aa1e1e'>
%sapper.base%
<link rel='stylesheet' href='global.css'>
<link rel='manifest' href='manifest.json'>
<link rel='icon' type='image/png' href='favicon.png'>
<!-- Sapper generates a <style> tag containing critical CSS
for the current page. CSS for the rest of the app is
lazily loaded when it precaches secondary pages -->
%sapper.styles%
<!-- This contains the contents of the <:Head> component, if
the current page has one -->
%sapper.head%
</head>
<body>
<!-- The application will be rendered inside this element,
because `templates/main.js` references it -->
<div id='sapper'>%sapper.html%</div>
<!-- Sapper creates a <script> tag containing `templates/main.js`
and anything else it needs to hydrate the app and
initialise the router -->
%sapper.scripts%
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1,45 +0,0 @@
body {
margin: 0;
font-family: Roboto, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
font-size: 14px;
line-height: 1.5;
color: #333;
}
main {
position: relative;
max-width: 56em;
background-color: white;
padding: 2em;
margin: 0 auto;
box-sizing: border-box;
}
h1, h2, h3, h4, h5, h6 {
margin: 0 0 0.5em 0;
font-weight: 400;
line-height: 1.2;
}
h1 {
font-size: 2em;
}
a {
color: inherit;
}
code {
font-family: menlo, inconsolata, monospace;
font-size: calc(1em - 2px);
color: #555;
background-color: #f0f0f0;
padding: 0.2em 0.4em;
border-radius: 2px;
}
@media (min-width: 400px) {
body {
font-size: 16px;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

View File

@@ -1,20 +0,0 @@
{
"background_color": "#ffffff",
"theme_color": "#aa1e1e",
"name": "TODO",
"short_name": "TODO",
"display": "minimal-ui",
"start_url": "/",
"icons": [
{
"src": "svelte-logo-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "svelte-logo-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -1,84 +0,0 @@
const path = require('path');
const webpack = require('webpack');
const config = require('../../config/webpack.js');
const sapper_pkg = require('../../package.json');
const mode = process.env.NODE_ENV;
const isDev = mode === 'development';
module.exports = {
client: {
entry: config.client.entry(),
output: config.client.output(),
resolve: {
extensions: ['.js', '.html']
},
module: {
rules: [
{
test: /\.html$/,
exclude: /node_modules/,
use: {
loader: 'svelte-loader',
options: {
hydratable: true,
cascade: false,
store: true
}
}
}
]
},
mode,
optimization: {
minimize: false
},
plugins: [
isDev && new webpack.HotModuleReplacementPlugin()
].filter(Boolean),
devtool: isDev && 'inline-source-map'
},
server: {
entry: config.server.entry(),
output: config.server.output(),
target: 'node',
resolve: {
extensions: ['.js', '.html']
},
externals: [].concat(
Object.keys(sapper_pkg.dependencies),
Object.keys(sapper_pkg.devDependencies)
),
module: {
rules: [
{
test: /\.html$/,
exclude: /node_modules/,
use: {
loader: 'svelte-loader',
options: {
css: false,
cascade: false,
store: true,
generate: 'ssr'
}
}
}
]
},
mode,
optimization: {
minimize: false
},
performance: {
hints: false // it doesn't matter if server.js is large
}
},
serviceworker: {
entry: config.serviceworker.entry(),
output: config.serviceworker.output(),
mode
}
};

123
test/apps/AppRunner.ts Normal file
View File

@@ -0,0 +1,123 @@
import * as path from 'path';
import * as puppeteer from 'puppeteer';
import * as ports from 'port-authority';
import { fork, ChildProcess } from 'child_process';
declare const start: () => Promise<void>;
declare const prefetchRoutes: () => Promise<void>;
declare const prefetch: (href: string) => Promise<void>;
declare const goto: (href: string) => Promise<void>;
export class AppRunner {
cwd: string;
entry: string;
port: number;
proc: ChildProcess;
messages: any[];
browser: puppeteer.Browser;
page: puppeteer.Page;
constructor(cwd: string, entry: string) {
this.cwd = cwd;
this.entry = path.join(cwd, entry);
this.messages = [];
}
async start() {
this.port = await ports.find(3000);
this.proc = fork(this.entry, [], {
cwd: this.cwd,
env: {
PORT: String(this.port)
}
});
this.proc.on('message', message => {
if (!message.__sapper__) return;
this.messages.push(message);
});
this.browser = await puppeteer.launch({ args: ['--no-sandbox'] });
this.page = await this.browser.newPage();
this.page.on('console', msg => {
const text = msg.text();
if (!text.startsWith('Failed to load resource')) {
console.log(text);
}
});
return {
page: this.page,
base: `http://localhost:${this.port}`,
// helpers
start: () => this.page.evaluate(() => start()),
prefetchRoutes: () => this.page.evaluate(() => prefetchRoutes()),
prefetch: (href: string) => this.page.evaluate((href: string) => prefetch(href), href),
goto: (href: string) => this.page.evaluate((href: string) => goto(href), href),
title: () => this.page.$eval('h1', node => node.textContent)
};
}
capture(fn: () => any): Promise<string[]> {
return new Promise((fulfil, reject) => {
const requests: string[] = [];
const pending: Set<string> = new Set();
let done = false;
function handle_request(request: puppeteer.Request) {
const url = request.url();
requests.push(url);
pending.add(url);
}
function handle_requestfinished(request: puppeteer.Request) {
const url = request.url();
pending.delete(url);
if (done && pending.size === 0) {
cleanup();
fulfil(requests);
}
}
function handle_requestfailed(request: puppeteer.Request) {
cleanup();
reject(new Error(`failed to fetch ${request.url()}`))
}
const cleanup = () => {
this.page.removeListener('request', handle_request);
this.page.removeListener('requestfinished', handle_requestfinished);
this.page.removeListener('requestfailed', handle_requestfailed);
};
this.page.on('request', handle_request);
this.page.on('requestfinished', handle_requestfinished);
this.page.on('requestfailed', handle_requestfailed);
return Promise.resolve(fn()).then(() => {
if (pending.size === 0) {
cleanup();
fulfil(requests);
}
done = true;
});
});
}
end() {
return Promise.all([
this.browser.close(),
new Promise(fulfil => {
this.proc.once('exit', fulfil);
this.proc.kill();
})
]);
}
}

View File

@@ -0,0 +1,64 @@
import resolve from 'rollup-plugin-node-resolve';
import replace from 'rollup-plugin-replace';
import svelte from 'rollup-plugin-svelte';
const mode = process.env.NODE_ENV;
const dev = mode === 'development';
const config = require('../../../config/rollup.js');
export default {
client: {
input: config.client.input(),
output: config.client.output(),
plugins: [
replace({
'process.browser': true,
'process.env.NODE_ENV': JSON.stringify(mode)
}),
svelte({
dev,
hydratable: true,
emitCss: true
}),
resolve()
],
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
},
server: {
input: config.server.input(),
output: config.server.output(),
plugins: [
replace({
'process.browser': false,
'process.env.NODE_ENV': JSON.stringify(mode)
}),
svelte({
generate: 'ssr',
dev
}),
resolve({
preferBuiltins: true
})
],
external: ['sirv', 'polka'],
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
},
serviceworker: {
input: config.serviceworker.input(),
output: config.serviceworker.output(),
plugins: [
resolve(),
replace({
'process.browser': true,
'process.env.NODE_ENV': JSON.stringify(mode)
})
]
}
};

View File

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

View File

@@ -0,0 +1 @@
<h1>{params.slug.toUpperCase()}</h1>

View File

@@ -0,0 +1,3 @@
<h1>{status}</h1>
<p>{error.message}</p>

View File

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

View File

@@ -0,0 +1,3 @@
export function get(req, res) {
res.end(req.params.slug);
}

View File

@@ -0,0 +1,11 @@
<h1>{letter}</h1>
<script>
export default {
preload() {
return this.fetch('b.json').then(r => r.json()).then(letter => {
return { letter };
});
}
};
</script>

View File

@@ -0,0 +1,3 @@
export function get(req, res) {
res.end(JSON.stringify('b'));
}

View File

@@ -2,9 +2,13 @@
<script>
export default {
oncreate() {
window.deleted = null;
},
methods: {
del() {
fetch(`api/delete/42`, { method: 'DELETE' })
fetch(`delete-test/42.json`, { method: 'DELETE' })
.then(r => r.json())
.then(data => {
window.deleted = data;

View File

@@ -0,0 +1,9 @@
<h1>message: "{message}"</h1>
<script>
export default {
preload({ query }) {
return query;
}
};
</script>

View File

@@ -0,0 +1,7 @@
<h1>Great success!</h1>
<a href="a">a</a>
<a href="ambiguous/ok.json">ok</a>
<a href="echo-query?message">ok</a>
<div class='hydrate-test'></div>

View File

@@ -0,0 +1,8 @@
import polka from 'polka';
import * as sapper from '../__sapper__/server.js';
const { PORT } = process.env;
polka()
.use(sapper.middleware())
.listen(PORT);

View File

@@ -1,10 +1,10 @@
import { files, shell, timestamp, routes } from '../__sapper__/service-worker.js';
import { timestamp, files, shell, routes } from '../__sapper__/service-worker.js';
const ASSETS = `cachetimestamp`;
const ASSETS = `cache${timestamp}`;
// `shell` is an array of all the files generated by webpack,
// `assets` is an array of everything in the `assets` directory
const to_cache = shell.concat(files);
// `files` is an array of everything in the `static` directory
const to_cache = shell.concat(ASSETS);
const cached = new Set(to_cache);
self.addEventListener('install', event => {
@@ -26,19 +26,24 @@ self.addEventListener('activate', event => {
if (key !== ASSETS) await caches.delete(key);
}
await self.clients.claim();
self.clients.claim();
})
);
});
self.addEventListener('fetch', event => {
if (event.request.method !== 'GET') return;
const url = new URL(event.request.url);
// don't try to handle e.g. data: URIs
if (!url.protocol.startsWith('http')) return;
// ignore dev server requests
if (url.hostname === self.location.hostname && url.port !== self.location.port) return;
// always serve assets and webpack-generated files from cache
if (cached.has(url.pathname)) {
if (url.host === self.location.host && cached.has(url.pathname)) {
event.respondWith(caches.match(event.request));
return;
}
@@ -53,12 +58,14 @@ self.addEventListener('fetch', event => {
}
*/
if (event.request.cache === 'only-if-cached') return;
// for everything else, try the network first, falling back to
// cache if the user is offline. (If the pages never change, you
// might prefer a cache-first approach to a network-first one.)
event.respondWith(
caches
.open('offline')
.open(`offline${timestamp}`)
.then(async cache => {
try {
const response = await fetch(event.request);

View File

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

246
test/apps/basics/test.ts Normal file
View File

@@ -0,0 +1,246 @@
import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import * as http from 'http';
import { build } from '../../../api';
import { AppRunner } from '../AppRunner';
import { wait } from '../../utils';
declare let deleted: { id: number };
declare let el: any;
describe('basics', function() {
this.timeout(10000);
let runner: AppRunner;
let page: puppeteer.Page;
let base: string;
// helpers
let start: () => Promise<void>;
let prefetchRoutes: () => Promise<void>;
let prefetch: (href: string) => Promise<void>;
let goto: (href: string) => Promise<void>;
// hooks
before(async () => {
await build({ cwd: __dirname });
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes, prefetch, goto } = await runner.start());
});
after(() => runner.end());
const title = () => page.$eval('h1', node => node.textContent);
it('serves /', async () => {
await page.goto(base);
assert.equal(
await title(),
'Great success!'
);
});
it('serves /?', async () => {
await page.goto(`${base}?`);
assert.equal(
await title(),
'Great success!'
);
});
it('serves static route', async () => {
await page.goto(`${base}/a`);
assert.equal(
await title(),
'a'
);
});
it('serves static route from dir/index.html file', async () => {
await page.goto(`${base}/b`);
assert.equal(
await title(),
'b'
);
});
it('serves dynamic route', async () => {
await page.goto(`${base}/test-slug`);
assert.equal(
await title(),
'TEST-SLUG'
);
});
it('navigates to a new page without reloading', async () => {
await page.goto(base);
await start();
await prefetchRoutes();
const requests: string[] = await runner.capture(async () => {
await page.click('a[href="a"]');
});
assert.deepEqual(requests, []);
assert.equal(
await title(),
'a'
);
});
it('navigates programmatically', async () => {
await page.goto(`${base}/a`);
await start();
await goto('b');
assert.equal(
await title(),
'b'
);
});
it('prefetches programmatically', async () => {
await page.goto(`${base}/a`);
await start();
const requests = await runner.capture(() => prefetch('b'));
assert.equal(requests.length, 2);
assert.equal(requests[1], `${base}/b.json`);
});
// TODO equivalent test for a webpack app
it('sets Content-Type, Link...modulepreload, and Cache-Control headers', () => {
return new Promise((fulfil, reject) => {
const req = http.get(base, res => {
try {
const { headers } = res;
assert.equal(
headers['content-type'],
'text/html'
);
assert.equal(
headers['cache-control'],
'max-age=600'
);
// TODO preload more than just the entry point
const regex = /<\/client\/client\.\w+\.js>;rel="modulepreload"/;
const link = <string>headers['link'];
assert.ok(regex.test(link), link);
fulfil();
} catch (err) {
reject(err);
}
});
req.on('error', reject);
});
});
it('calls a delete handler', async () => {
await page.goto(`${base}/delete-test`);
await start();
await page.click('.del');
await page.waitForFunction(() => deleted);
assert.equal(await page.evaluate(() => deleted.id), 42);
});
it('hydrates initial route', async () => {
await page.goto(base);
await page.evaluate(() => {
el = document.querySelector('.hydrate-test');
});
await start();
assert.ok(await page.evaluate(() => {
return document.querySelector('.hydrate-test') === el;
}));
});
it('does not attempt client-side navigation to server routes', async () => {
await page.goto(base);
await start();
await prefetchRoutes();
await page.click(`[href="ambiguous/ok.json"]`);
await wait(50);
assert.equal(
await page.evaluate(() => document.body.textContent),
'ok'
);
});
it('allows reserved words as route names', async () => {
await page.goto(`${base}/const`);
await start();
assert.equal(
await title(),
'reserved words are okay as routes'
);
});
it('accepts value-less query string parameter on server', async () => {
await page.goto(`${base}/echo-query?message`);
assert.equal(
await title(),
'message: ""'
);
});
it('accepts value-less query string parameter on client', async () => {
await page.goto(base);
await start();
await prefetchRoutes();
await page.click('a[href="echo-query?message"]')
assert.equal(
await title(),
'message: ""'
);
});
// skipped because Nightmare doesn't seem to focus the <a> correctly
it('resets the active element after navigation', async () => {
await page.goto(base);
await start();
await prefetchRoutes();
await page.click('[href="a"]');
await wait(50);
assert.equal(
await page.evaluate(() => document.activeElement.nodeName),
'BODY'
);
});
it('replaces %sapper.xxx% tags safely', async () => {
await page.goto(`${base}/unsafe-replacement`);
await start();
const html = await page.evaluate(() => document.body.innerHTML);
assert.equal(html.indexOf('%sapper'), -1);
});
});

View File

@@ -0,0 +1,64 @@
import resolve from 'rollup-plugin-node-resolve';
import replace from 'rollup-plugin-replace';
import svelte from 'rollup-plugin-svelte';
const mode = process.env.NODE_ENV;
const dev = mode === 'development';
const config = require('../../../config/rollup.js');
export default {
client: {
input: config.client.input(),
output: config.client.output(),
plugins: [
replace({
'process.browser': true,
'process.env.NODE_ENV': JSON.stringify(mode)
}),
svelte({
dev,
hydratable: true,
emitCss: true
}),
resolve()
],
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
},
server: {
input: config.server.input(),
output: config.server.output(),
plugins: [
replace({
'process.browser': false,
'process.env.NODE_ENV': JSON.stringify(mode)
}),
svelte({
generate: 'ssr',
dev
}),
resolve({
preferBuiltins: true
})
],
external: ['sirv', 'polka', 'cookie'],
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
},
serviceworker: {
input: config.serviceworker.input(),
output: config.serviceworker.output(),
plugins: [
resolve(),
replace({
'process.browser': true,
'process.env.NODE_ENV': JSON.stringify(mode)
})
]
}
};

View File

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

View File

@@ -0,0 +1,3 @@
<h1>{status}</h1>
<p>{error.message}</p>

View File

@@ -0,0 +1,3 @@
<h1>Great success!</h1>
<a href="credentials?creds=include">link</a>

View File

@@ -0,0 +1,13 @@
import polka from 'polka';
import * as sapper from '../__sapper__/server.js';
const { PORT } = process.env;
polka()
.use((req, res, next) => {
// set test cookie
res.setHeader('Set-Cookie', ['a=1; Max-Age=3600', 'b=2; Max-Age=3600']);
next();
})
.use(sapper.middleware())
.listen(PORT);

View File

@@ -0,0 +1,82 @@
import { timestamp, files, shell, routes } from '../__sapper__/service-worker.js';
const ASSETS = `cache${timestamp}`;
// `shell` is an array of all the files generated by webpack,
// `files` is an array of everything in the `static` directory
const to_cache = shell.concat(ASSETS);
const cached = new Set(to_cache);
self.addEventListener('install', event => {
event.waitUntil(
caches
.open(ASSETS)
.then(cache => cache.addAll(to_cache))
.then(() => {
self.skipWaiting();
})
);
});
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(async keys => {
// delete old caches
for (const key of keys) {
if (key !== ASSETS) await caches.delete(key);
}
self.clients.claim();
})
);
});
self.addEventListener('fetch', event => {
if (event.request.method !== 'GET') return;
const url = new URL(event.request.url);
// don't try to handle e.g. data: URIs
if (!url.protocol.startsWith('http')) return;
// ignore dev server requests
if (url.hostname === self.location.hostname && url.port !== self.location.port) return;
// always serve assets and webpack-generated files from cache
if (url.host === self.location.host && cached.has(url.pathname)) {
event.respondWith(caches.match(event.request));
return;
}
// for pages, you might want to serve a shell `index.html` file,
// which Sapper has generated for you. It's not right for every
// app, but if it's right for yours then uncomment this section
/*
if (url.origin === self.origin && routes.find(route => route.pattern.test(url.pathname))) {
event.respondWith(caches.match('/index.html'));
return;
}
*/
if (event.request.cache === 'only-if-cached') return;
// for everything else, try the network first, falling back to
// cache if the user is offline. (If the pages never change, you
// might prefer a cache-first approach to a network-first one.)
event.respondWith(
caches
.open(`offline${timestamp}`)
.then(async cache => {
try {
const response = await fetch(event.request);
cache.put(event.request, response.clone());
return response;
} catch(err) {
const response = await cache.match(event.request);
if (response) return response;
throw err;
}
})
);
});

View File

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

View File

@@ -0,0 +1,59 @@
import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import { build } from '../../../api';
import { wait } from '../../utils';
import { AppRunner } from '../AppRunner';
describe('credentials', function() {
this.timeout(10000);
let runner: AppRunner;
let page: puppeteer.Page;
let base: string;
// helpers
let start: () => Promise<void>;
let prefetchRoutes: () => Promise<void>;
// hooks
before(async () => {
await build({ cwd: __dirname });
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes } = await runner.start());
});
after(() => runner.end());
it('sends cookies when using this.fetch with credentials: "include"', async () => {
await page.goto(`${base}/credentials?creds=include`);
assert.equal(
await page.$eval('h1', node => node.textContent),
'a: 1, b: 2, max-age: undefined'
);
});
it('does not send cookies when using this.fetch without credentials', async () => {
await page.goto(`${base}/credentials`);
assert.equal(
await page.$eval('h1', node => node.textContent),
'unauthorized'
);
});
it('delegates to fetch on the client', async () => {
await page.goto(base)
await start();
await prefetchRoutes();
await page.click('[href="credentials?creds=include"]');
await wait(50);
assert.equal(
await page.$eval('h1', node => node.textContent),
'a: 1, b: 2, max-age: undefined'
);
});
});

View File

@@ -0,0 +1,64 @@
import resolve from 'rollup-plugin-node-resolve';
import replace from 'rollup-plugin-replace';
import svelte from 'rollup-plugin-svelte';
const mode = process.env.NODE_ENV;
const dev = mode === 'development';
const config = require('../../../config/rollup.js');
export default {
client: {
input: config.client.input(),
output: config.client.output(),
plugins: [
replace({
'process.browser': true,
'process.env.NODE_ENV': JSON.stringify(mode)
}),
svelte({
dev,
hydratable: true,
emitCss: true
}),
resolve()
],
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
},
server: {
input: config.server.input(),
output: config.server.output(),
plugins: [
replace({
'process.browser': false,
'process.env.NODE_ENV': JSON.stringify(mode)
}),
svelte({
generate: 'ssr',
dev
}),
resolve({
preferBuiltins: true
})
],
external: ['sirv', 'polka'],
// temporary, pending Rollup 1.0
experimentalCodeSplitting: true
},
serviceworker: {
input: config.serviceworker.input(),
output: config.serviceworker.output(),
plugins: [
resolve(),
replace({
'process.browser': true,
'process.env.NODE_ENV': JSON.stringify(mode)
})
]
}
};

View File

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

View File

@@ -0,0 +1,7 @@
<h1>Title</h1>
<style>
h1 {
color: green;
}
</style>

View File

@@ -0,0 +1,3 @@
<h1>{status}</h1>
<p>{error.message}</p>

View File

@@ -0,0 +1,11 @@
<svelte:component this={Title}/>
<script>
export default {
oncreate() {
import('./_components/Title.html').then(({ default: Title }) => {
this.set({ Title });
});
}
};
</script>

View File

@@ -0,0 +1,7 @@
<h1>Foo</h1>
<style>
h1 {
color: blue;
}
</style>

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