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
## 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
* Whoops — `tslib` is a runtime dependency

View File

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

View File

@@ -13,6 +13,18 @@ export async function build() {
mkdirp.sync(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();
// 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(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) {

View File

@@ -71,6 +71,13 @@ function create_hot_update_server(port: number, interval = 10000) {
}
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';
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);
watch_files(`${locations.routes()}/**/*`, ['add', 'unlink'], () => {
watch_files(locations.routes(), ['add', 'unlink'], () => {
const routes = create_routes();
create_main_manifests({ routes, dev_port });
});
@@ -304,7 +311,8 @@ function watch_files(pattern: string, events: string[], callback: () => void) {
const watcher = chokidar.watch(pattern, {
persistent: true,
ignoreInitial: true
ignoreInitial: true,
disableGlobbing: true
});
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`), [], {
cwd: process.cwd(),
env: {
env: Object.assign({
PORT: port,
NODE_ENV: 'production',
SAPPER_DEST: build_dir,
SAPPER_EXPORT: 'true'
}
}, process.env)
});
const seen = new Set();
@@ -103,4 +103,4 @@ export async function exporter(export_dir: string, { basepath = '' }) {
return ports.wait(port)
.then(() => handle(new URL(`/${basepath}`, origin))) // TODO all static routes
.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');
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;
}
if (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;
}
} else {

View File

@@ -3,7 +3,7 @@ import glob from 'glob';
import { locations } from '../config';
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
.map((file: string) => {
if (/(^|\/|\\)_/.test(file)) return;
@@ -33,7 +33,7 @@ export default function create_routes({ files } = { files: glob.sync('**/*.*', {
let i = parts.length;
let nested = true;
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('[');
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) {
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 };
},
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) {
opts = Object.assign({}, opts);
@@ -245,11 +245,11 @@ function get_route_handler(chunks: Record<string, string>, routes: RouteObject[]
`baseUrl: "${req.baseUrl}"`,
serialized.preloaded && `preloaded: ${serialized.preloaded}`,
serialized.store && `store: ${serialized.store}`
].filter(Boolean).join(',')}}`
].filter(Boolean).join(',')}};`;
const has_service_worker = fs.existsSync(path.join(locations.dest(), 'service-worker.js'));
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()
@@ -356,6 +356,8 @@ function get_route_handler(chunks: Record<string, string>, routes: RouteObject[]
const rendered = route ? route.module.render({
status: statusCode,
error
}, {
store: store_getter && store_getter(req)
}) : { head: '', css: null, html: title };
const { head, css, html } = rendered;

View File

@@ -1,6 +1,6 @@
<:Head>
<title>{{status}}</title>
</:Head>
<svelte:head>
<title>{status}</title>
</svelte:head>
<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>
</:Head>
</svelte:head>
<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>
</:Head>
</svelte:head>
<h1>About this site</h1>

View File

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

View File

@@ -1,17 +1,17 @@
<:Head>
<svelte:head>
<title>Blog</title>
</:Head>
</svelte:head>
<h1>Recent posts</h1>
<ul>
{{#each posts as post}}
{#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}}
<li><a rel='prefetch' href='blog/{post.slug}'>{post.title}</a></li>
{/each}
</ul>
<script>

View File

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

View File

@@ -1,6 +1,6 @@
<:Head>
<svelte:head>
<title>Sapper project template</title>
</:Head>
</svelte:head>
<h1>Great success!</h1>
@@ -11,7 +11,7 @@
<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 rel=prefetch class='{page === "blog" ? "selected" : ""}' href='blog'>blog</a>
<div class='hydrate-test'></div>

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
<p>URL is {{url}}</p>
<p>URL is {url}</p>
<script>
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!');
});
});
it('includes service worker', () => {
return nightmare.goto(base).page.html().then(html => {
assert.ok(html.indexOf('service-worker.js') !== -1);
});
});
});
describe('headers', () => {

View File

@@ -2,6 +2,25 @@ const assert = require('assert');
const { create_routes } = require('../../dist/core.ts.js');
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', () => {
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']
@@ -12,8 +31,8 @@ describe('create_routes', () => {
[
'index.html',
'about.html',
'post/foo.html',
'post/bar.html',
'post/foo.html',
'post/f[xx].html',
'post/[id].json.js',
'post/[id].html',
@@ -23,7 +42,7 @@ describe('create_routes', () => {
});
it('prefers index page to nested route', () => {
const routes = create_routes({
let routes = create_routes({
files: [
'api/examples/[slug].js',
'api/examples/index.js',
@@ -55,6 +74,45 @@ describe('create_routes', () => {
'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', () => {