Merge branch 'master' into gh-140

This commit is contained in:
Rich Harris
2018-05-03 21:54:52 -04:00
21 changed files with 181 additions and 58 deletions

View File

@@ -1,5 +1,45 @@
# sapper changelog # sapper changelog
## 0.10.7
* Allow routes to have a leading `.` ([#243](https://github.com/sveltejs/sapper/pull/243))
* Only encode necessary characters in routes ([#234](https://github.com/sveltejs/sapper/pull/234))
* Preserve existing `process.env` when exporting ([#245](https://github.com/sveltejs/sapper/pull/245))
## 0.10.6
* Fix error reporting in `sapper start`
## 0.10.5
* Fix missing service worker ([#231](https://github.com/sveltejs/sapper/pull/231))
## 0.10.4
* Upgrade chokidar, this time with a fix ([#227](https://github.com/sveltejs/sapper/pull/227))
## 0.10.3
* Downgrade chokidar ([#212](https://github.com/sveltejs/sapper/issues/212))
## 0.10.2
* Attach `store` to error pages
* Fix sorting edge case ([#215](https://github.com/sveltejs/sapper/pull/215))
## 0.10.1
* Fix server-side `fetch` paths ([#207](https://github.com/sveltejs/sapper/pull/207))
## 0.10.0
* Support mounting on a path (this requires `app/template.html` to include `%sapper.base%`) ([#180](https://github.com/sveltejs/sapper/issues/180))
* Support per-request server-side `Store` with client-side hydration ([#178](https://github.com/sveltejs/sapper/issues/178))
* Add `this.fetch` to `preload`, with credentials support ([#178](https://github.com/sveltejs/sapper/issues/178))
* Exclude sourcemaps from preload links and `<script>` block ([#204](https://github.com/sveltejs/sapper/pull/204))
* Register service worker in `<script>` block
## 0.9.6 ## 0.9.6
* Whoops — `tslib` is a runtime dependency * Whoops — `tslib` is a runtime dependency

View File

@@ -1,6 +1,6 @@
{ {
"name": "sapper", "name": "sapper",
"version": "0.9.6", "version": "0.10.7",
"description": "Military-grade apps, engineered by Svelte", "description": "Military-grade apps, engineered by Svelte",
"main": "dist/middleware.ts.js", "main": "dist/middleware.ts.js",
"bin": { "bin": {
@@ -19,7 +19,7 @@
}, },
"dependencies": { "dependencies": {
"cheerio": "^1.0.0-rc.2", "cheerio": "^1.0.0-rc.2",
"chokidar": "^2.0.2", "chokidar": "^2.0.3",
"clorox": "^1.0.3", "clorox": "^1.0.3",
"cookie": "^0.3.1", "cookie": "^0.3.1",
"devalue": "^1.0.1", "devalue": "^1.0.1",
@@ -34,13 +34,13 @@
"rimraf": "^2.6.2", "rimraf": "^2.6.2",
"sade": "^1.4.0", "sade": "^1.4.0",
"sander": "^0.6.0", "sander": "^0.6.0",
"source-map-support": "^0.5.4", "source-map-support": "^0.5.5",
"tslib": "^1.9.0", "tslib": "^1.9.0",
"url-parse": "^1.2.0", "url-parse": "^1.2.0",
"webpack-format-messages": "^1.0.2" "webpack-format-messages": "^1.0.2"
}, },
"devDependencies": { "devDependencies": {
"@std/esm": "^0.25.3", "@std/esm": "^0.26.0",
"@types/glob": "^5.0.34", "@types/glob": "^5.0.34",
"@types/mkdirp": "^0.5.2", "@types/mkdirp": "^0.5.2",
"@types/rimraf": "^2.0.2", "@types/rimraf": "^2.0.2",
@@ -53,16 +53,16 @@
"nightmare": "^3.0.0", "nightmare": "^3.0.0",
"npm-run-all": "^4.1.2", "npm-run-all": "^4.1.2",
"polka": "^0.3.4", "polka": "^0.3.4",
"rollup": "^0.57.0", "rollup": "^0.58.2",
"rollup-plugin-commonjs": "^9.1.0", "rollup-plugin-commonjs": "^9.1.3",
"rollup-plugin-json": "^2.3.0", "rollup-plugin-json": "^2.3.0",
"rollup-plugin-string": "^2.0.2", "rollup-plugin-string": "^2.0.2",
"rollup-plugin-typescript": "^0.8.1", "rollup-plugin-typescript": "^0.8.1",
"serve-static": "^1.13.2", "serve-static": "^1.13.2",
"svelte": "^1.57.4", "svelte": "^2.4.4",
"svelte-loader": "^2.5.1", "svelte-loader": "^2.9.0",
"ts-node": "^5.0.1", "ts-node": "^6.0.2",
"typescript": "^2.6.2", "typescript": "^2.8.3",
"walk-sync": "^0.3.2", "walk-sync": "^0.3.2",
"webpack": "^4.1.0" "webpack": "^4.1.0"
}, },

View File

@@ -13,6 +13,18 @@ export async function build() {
mkdirp.sync(output); mkdirp.sync(output);
rimraf.sync(path.join(output, '**/*')); rimraf.sync(path.join(output, '**/*'));
// minify app/template.html
// TODO compile this to a function? could be quicker than str.replace(...).replace(...).replace(...)
const template = fs.readFileSync(`${locations.app()}/template.html`, 'utf-8');
// remove this in a future version
if (template.indexOf('%sapper.base%') === -1) {
console.log(`${clorox.bold.red(`> As of Sapper v0.10, your template.html file must include %sapper.base% in the <head>`)}`);
process.exit(1);
}
fs.writeFileSync(`${output}/template.html`, minify_html(template));
const routes = create_routes(); const routes = create_routes();
// create app/manifest/client.js and app/manifest/server.js // create app/manifest/client.js and app/manifest/server.js
@@ -41,11 +53,6 @@ export async function build() {
console.log(`${clorox.inverse(`\nbuilt service worker`)}`); console.log(`${clorox.inverse(`\nbuilt service worker`)}`);
console.log(serviceworker_stats.toString({ colors: true })); console.log(serviceworker_stats.toString({ colors: true }));
} }
// minify app/template.html
// TODO compile this to a function? could be quicker than str.replace(...).replace(...).replace(...)
const template = fs.readFileSync(`${locations.app()}/template.html`, 'utf-8');
fs.writeFileSync(`${output}/template.html`, minify_html(template));
} }
function compile(compiler: any) { function compile(compiler: any) {

View File

@@ -71,6 +71,13 @@ function create_hot_update_server(port: number, interval = 10000) {
} }
export async function dev(opts: { port: number, open: boolean }) { export async function dev(opts: { port: number, open: boolean }) {
// remove this in a future version
const template = fs.readFileSync(path.join(locations.app(), 'template.html'), 'utf-8');
if (template.indexOf('%sapper.base%') === -1) {
console.log(`${clorox.bold.red(`> As of Sapper v0.10, your template.html file must include %sapper.base% in the <head>`)}`);
process.exit(1);
}
process.env.NODE_ENV = 'development'; process.env.NODE_ENV = 'development';
let port = opts.port || +process.env.PORT; let port = opts.port || +process.env.PORT;
@@ -95,7 +102,7 @@ export async function dev(opts: { port: number, open: boolean }) {
const hot_update_server = create_hot_update_server(dev_port); const hot_update_server = create_hot_update_server(dev_port);
watch_files(`${locations.routes()}/**/*`, ['add', 'unlink'], () => { watch_files(locations.routes(), ['add', 'unlink'], () => {
const routes = create_routes(); const routes = create_routes();
create_main_manifests({ routes, dev_port }); create_main_manifests({ routes, dev_port });
}); });
@@ -304,7 +311,8 @@ function watch_files(pattern: string, events: string[], callback: () => void) {
const watcher = chokidar.watch(pattern, { const watcher = chokidar.watch(pattern, {
persistent: true, persistent: true,
ignoreInitial: true ignoreInitial: true,
disableGlobbing: true
}); });
events.forEach(event => { events.forEach(event => {

View File

@@ -35,12 +35,12 @@ export async function exporter(export_dir: string, { basepath = '' }) {
const proc = child_process.fork(path.resolve(`${build_dir}/server.js`), [], { const proc = child_process.fork(path.resolve(`${build_dir}/server.js`), [], {
cwd: process.cwd(), cwd: process.cwd(),
env: { env: Object.assign({
PORT: port, PORT: port,
NODE_ENV: 'production', NODE_ENV: 'production',
SAPPER_DEST: build_dir, SAPPER_DEST: build_dir,
SAPPER_EXPORT: 'true' SAPPER_EXPORT: 'true'
} }, process.env)
}); });
const seen = new Set(); const seen = new Set();
@@ -103,4 +103,4 @@ export async function exporter(export_dir: string, { basepath = '' }) {
return ports.wait(port) return ports.wait(port)
.then(() => handle(new URL(`/${basepath}`, origin))) // TODO all static routes .then(() => handle(new URL(`/${basepath}`, origin))) // TODO all static routes
.then(() => proc.kill()); .then(() => proc.kill());
} }

View File

@@ -11,13 +11,13 @@ export async function start(dir: string, opts: { port: number, open: boolean })
const server = path.resolve(dir, 'server.js'); const server = path.resolve(dir, 'server.js');
if (!fs.existsSync(server)) { if (!fs.existsSync(server)) {
console.log(clorox.bold.red(`> ${dir}/server.js does not exist — type ${clorox.bold.cyan(dir === 'build' ? `npx sapper build` : `npx sapper build ${dir}`)} to create it`)); console.log(`${clorox.bold.red(`> ${dir}/server.js does not exist — type ${clorox.bold.cyan(dir === 'build' ? `npx sapper build` : `npx sapper build ${dir}`)} to create it`)}`);
return; return;
} }
if (port) { if (port) {
if (!await ports.check(port)) { if (!await ports.check(port)) {
console.log(clorox.bold.red(`> Port ${port} is unavailable`)); console.log(`${clorox.bold.red(`> Port ${port} is unavailable`)}`);
return; return;
} }
} else { } else {

View File

@@ -3,7 +3,7 @@ import glob from 'glob';
import { locations } from '../config'; import { locations } from '../config';
import { Route } from '../interfaces'; import { Route } from '../interfaces';
export default function create_routes({ files } = { files: glob.sync('**/*.*', { cwd: locations.routes(), nodir: true }) }) { export default function create_routes({ files } = { files: glob.sync('**/*.*', { cwd: locations.routes(), dot: true, nodir: true }) }) {
const routes: Route[] = files const routes: Route[] = files
.map((file: string) => { .map((file: string) => {
if (/(^|\/|\\)_/.test(file)) return; if (/(^|\/|\\)_/.test(file)) return;
@@ -33,7 +33,7 @@ export default function create_routes({ files } = { files: glob.sync('**/*.*', {
let i = parts.length; let i = parts.length;
let nested = true; let nested = true;
while (i--) { while (i--) {
const part = encodeURIComponent(parts[i].normalize()).replace(/%5B/g, '[').replace(/%5D/g, ']'); const part = encodeURI(parts[i].normalize()).replace(/\?/g, '%3F').replace(/#/g, '%23').replace(/%5B/g, '[').replace(/%5D/g, ']');
const dynamic = ~part.indexOf('['); const dynamic = ~part.indexOf('[');
if (dynamic) { if (dynamic) {
@@ -102,7 +102,10 @@ export default function create_routes({ files } = { files: glob.sync('**/*.*', {
} }
if (!a_sub_part.dynamic && a_sub_part.content !== b_sub_part.content) { if (!a_sub_part.dynamic && a_sub_part.content !== b_sub_part.content) {
return b_sub_part.content.length - a_sub_part.content.length; return (
(b_sub_part.content.length - a_sub_part.content.length) ||
(a_sub_part.content < b_sub_part.content ? -1 : 1)
);
} }
} }
} }

View File

@@ -173,7 +173,7 @@ function get_route_handler(chunks: Record<string, string>, routes: RouteObject[]
error = { statusCode, message }; error = { statusCode, message };
}, },
fetch: (url: string, opts?: any) => { fetch: (url: string, opts?: any) => {
const parsed = new URL(url, `http://127.0.0.1:${process.env.PORT}${req.baseUrl}${req.path}`); const parsed = new URL(url, `http://127.0.0.1:${process.env.PORT}${req.baseUrl ? req.baseUrl + '/' :''}`);
if (opts) { if (opts) {
opts = Object.assign({}, opts); opts = Object.assign({}, opts);
@@ -245,11 +245,11 @@ function get_route_handler(chunks: Record<string, string>, routes: RouteObject[]
`baseUrl: "${req.baseUrl}"`, `baseUrl: "${req.baseUrl}"`,
serialized.preloaded && `preloaded: ${serialized.preloaded}`, serialized.preloaded && `preloaded: ${serialized.preloaded}`,
serialized.store && `store: ${serialized.store}` serialized.store && `store: ${serialized.store}`
].filter(Boolean).join(',')}}` ].filter(Boolean).join(',')}};`;
const has_service_worker = fs.existsSync(path.join(locations.dest(), 'service-worker.js')); const has_service_worker = fs.existsSync(path.join(locations.dest(), 'service-worker.js'));
if (has_service_worker) { if (has_service_worker) {
`if ('serviceWorker' in navigator) navigator.serviceWorker.register('${req.baseUrl}/service-worker.js')` inline_script += `if ('serviceWorker' in navigator) navigator.serviceWorker.register('${req.baseUrl}/service-worker.js');`;
} }
const page = template() const page = template()
@@ -356,6 +356,8 @@ function get_route_handler(chunks: Record<string, string>, routes: RouteObject[]
const rendered = route ? route.module.render({ const rendered = route ? route.module.render({
status: statusCode, status: statusCode,
error error
}, {
store: store_getter && store_getter(req)
}) : { head: '', css: null, html: title }; }) : { head: '', css: null, html: title };
const { head, css, html } = rendered; const { head, css, html } = rendered;

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
<:Head> <svelte:head>
<title>About</title> <title>About</title>
</:Head> </svelte:head>
<h1>About this site</h1> <h1>About this site</h1>

View File

@@ -1,11 +1,11 @@
<:Head> <svelte:head>
<title>{{post.title}}</title> <title>{post.title}</title>
</:Head> </svelte:head>
<h1>{{post.title}}</h1> <h1>{post.title}</h1>
<div class='content'> <div class='content'>
{{{post.html}}} {@html post.html}
</div> </div>
<script> <script>

View File

@@ -1,17 +1,17 @@
<:Head> <svelte:head>
<title>Blog</title> <title>Blog</title>
</:Head> </svelte:head>
<h1>Recent posts</h1> <h1>Recent posts</h1>
<ul> <ul>
{{#each posts as post}} {#each posts as post}
<!-- we're using the non-standard `rel=prefetch` attribute to <!-- we're using the non-standard `rel=prefetch` attribute to
tell Sapper to load the data for the page as soon as tell Sapper to load the data for the page as soon as
the user hovers over the link or taps it, instead of the user hovers over the link or taps it, instead of
waiting for the 'click' event --> waiting for the 'click' event -->
<li><a rel='prefetch' href='blog/{{post.slug}}'>{{post.title}}</a></li> <li><a rel='prefetch' href='blog/{post.slug}'>{post.title}</a></li>
{{/each}} {/each}
</ul> </ul>
<script> <script>

View File

@@ -1,9 +1,8 @@
<h1>{{message}}</h1> <h1>{message}</h1>
<script> <script>
export default { export default {
preload({ query }) { preload({ query }) {
console.log(`here ${this.fetch}`);
return this.fetch(`credentials/test.json`, { return this.fetch(`credentials/test.json`, {
credentials: query.creds credentials: query.creds
}).then(r => r.json()); }).then(r => r.json());

View File

@@ -1,6 +1,6 @@
<:Head> <svelte:head>
<title>Sapper project template</title> <title>Sapper project template</title>
</:Head> </svelte:head>
<h1>Great success!</h1> <h1>Great success!</h1>
@@ -11,7 +11,7 @@
<a href='blog/nope'>broken link</a> <a href='blog/nope'>broken link</a>
<a href='blog/throw-an-error'>error link</a> <a href='blog/throw-an-error'>error link</a>
<a href='credentials?creds=include'>credentials</a> <a href='credentials?creds=include'>credentials</a>
<a rel=prefetch class='{{page === "blog" ? "selected" : ""}}' href='blog'>blog</a> <a rel=prefetch class='{page === "blog" ? "selected" : ""}' href='blog'>blog</a>
<div class='hydrate-test'></div> <div class='hydrate-test'></div>

View File

@@ -1,4 +1,4 @@
<h1>{{foo.bar()}}</h1> <h1>{foo.bar()}</h1>
<script> <script>
export default { export default {

View File

@@ -1,4 +1,4 @@
<h1>{{set.has('x')}}</h1> <h1>{set.has('x')}</h1>
<script> <script>
export default { export default {

View File

@@ -1,4 +1,4 @@
<p>URL is {{url}}</p> <p>URL is {url}</p>
<script> <script>
export default { export default {

View File

@@ -1 +1 @@
<h1>{{$title}}</h1> <h1>{$title}</h1>

View File

@@ -562,6 +562,12 @@ function run({ mode, basepath = '' }) {
assert.equal(title, 'woohoo!'); assert.equal(title, 'woohoo!');
}); });
}); });
it('includes service worker', () => {
return nightmare.goto(base).page.html().then(html => {
assert.ok(html.indexOf('service-worker.js') !== -1);
});
});
}); });
describe('headers', () => { describe('headers', () => {

View File

@@ -2,6 +2,25 @@ const assert = require('assert');
const { create_routes } = require('../../dist/core.ts.js'); const { create_routes } = require('../../dist/core.ts.js');
describe('create_routes', () => { describe('create_routes', () => {
it('encodes caharcters not allowed in path', () => {
const routes = create_routes({
files: [
'"',
'#',
'?'
]
});
assert.deepEqual(
routes.map(r => r.pattern),
[
/^\/%22\/?$/,
/^\/%23\/?$/,
/^\/%3F\/?$/
]
);
});
it('sorts routes correctly', () => { it('sorts routes correctly', () => {
const routes = create_routes({ const routes = create_routes({
files: ['index.html', 'about.html', 'post/f[xx].html', '[wildcard].html', 'post/foo.html', 'post/[id].html', 'post/bar.html', 'post/[id].json.js'] files: ['index.html', 'about.html', 'post/f[xx].html', '[wildcard].html', 'post/foo.html', 'post/[id].html', 'post/bar.html', 'post/[id].json.js']
@@ -12,8 +31,8 @@ describe('create_routes', () => {
[ [
'index.html', 'index.html',
'about.html', 'about.html',
'post/foo.html',
'post/bar.html', 'post/bar.html',
'post/foo.html',
'post/f[xx].html', 'post/f[xx].html',
'post/[id].json.js', 'post/[id].json.js',
'post/[id].html', 'post/[id].html',
@@ -23,7 +42,7 @@ describe('create_routes', () => {
}); });
it('prefers index page to nested route', () => { it('prefers index page to nested route', () => {
const routes = create_routes({ let routes = create_routes({
files: [ files: [
'api/examples/[slug].js', 'api/examples/[slug].js',
'api/examples/index.js', 'api/examples/index.js',
@@ -55,6 +74,45 @@ describe('create_routes', () => {
'api/gists/[id].js', 'api/gists/[id].js',
] ]
); );
routes = create_routes({
files: [
'4xx.html',
'5xx.html',
'api/blog/[slug].js',
'api/blog/index.js',
'api/guide/contents.js',
'api/guide/index.js',
'blog/[slug].html',
'blog/index.html',
'blog/rss.xml.js',
'gist/[id].js',
'gist/create.js',
'guide/index.html',
'index.html',
'repl/index.html'
]
});
assert.deepEqual(
routes.map(r => r.file),
[
'4xx.html',
'5xx.html',
'index.html',
'guide/index.html',
'blog/index.html',
'blog/rss.xml.js',
'blog/[slug].html',
'gist/create.js',
'gist/[id].js',
'repl/index.html',
'api/guide/index.js',
'api/guide/contents.js',
'api/blog/index.js',
'api/blog/[slug].js',
]
);
}); });
it('generates params', () => { it('generates params', () => {