First, you have to know what Svelte 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 introductory blog post, you should!
-
-
Sapper is a Next.js-style framework (more on that here) built around Svelte. It makes it embarrassingly easy to create extremely high performance web apps. Out of the box, you get:
-
-
-
Code-splitting, dynamic imports and hot module replacement, powered by webpack
-
Server-side rendering (SSR) with client-side hydration
-
Service worker for offline support, and all the PWA bells and whistles
-
The nicest development experience you've ever had, or your money back
-
-
-
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.
- `
- },
-
- {
- title: 'How to use Sapper',
- slug: 'how-to-use-sapper',
- html: `
-
In war, the soldiers who build bridges, repair roads, clear minefields and conduct demolitions — all under combat conditions — are known as sappers.
-
-
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 Svelte app maker, is your courageous and dutiful ally.
- `
- },
-
- {
- title: 'How is Sapper different from Next.js?',
- slug: 'how-is-sapper-different-from-next',
- html: `
-
Next.js is a React framework from Zeit, and is the inspiration for Sapper. There are a few notable differences, however:
-
-
-
It's powered by Svelte instead of React, so it's faster and your apps are smaller
-
Instead of route masking, we encode route parameters in filenames. For example, the page you're looking at right now is routes/blog/[slug].html
-
As well as pages (Svelte components, which render on server or client), you can create server routes in your routes directory. These are just .js files that export functions corresponding to HTTP methods, and receive Express request and response objects as arguments. This makes it very easy to, for example, add a JSON API such as the one powering this very page
-
Links are just <a> elements, rather than framework-specific <Link> components. That means, for example, that this link right here, despite being inside a blob of HTML, works with the router as you'd expect.
-
- `
- },
-
- {
- title: 'How can I get involved?',
- slug: 'how-can-i-get-involved',
- html: `
-
We're so glad you asked! Come on over to the Svelte and Sapper repos, and join us in the Gitter chatroom. Everyone is welcome, especially you!
- `
- },
-
- {
- title: 'A very long post with deep links',
- slug: 'a-very-long-post',
- html: `
-
One
-
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.
-
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.
-
-
Two
-
Tobias Fünke costume. Heart attack never stopped old big bear.
-
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.
-
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.
-
-
Three
-
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!
-
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.
-
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.
-
-
Four
-
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!
-
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.
- `
- }
-];
-
-posts.forEach(post => {
- post.html = post.html.replace(/^\t{3}/gm, '');
-});
-
-export default posts;
\ No newline at end of file
diff --git a/test/app/routes/blog/index.html b/test/app/routes/blog/index.html
deleted file mode 100644
index 2e9f428..0000000
--- a/test/app/routes/blog/index.html
+++ /dev/null
@@ -1,25 +0,0 @@
-
- Blog
-
-
-
\ No newline at end of file
diff --git a/test/apps/basics/src/routes/_error.html b/test/apps/basics/src/routes/_error.html
new file mode 100644
index 0000000..4cd55d2
--- /dev/null
+++ b/test/apps/basics/src/routes/_error.html
@@ -0,0 +1,3 @@
+
{status}
+
+
{error.message}
\ No newline at end of file
diff --git a/test/apps/basics/src/routes/a.html b/test/apps/basics/src/routes/a.html
new file mode 100644
index 0000000..0e6757b
--- /dev/null
+++ b/test/apps/basics/src/routes/a.html
@@ -0,0 +1 @@
+
a
\ No newline at end of file
diff --git a/test/unit/create_routes/samples/basic/blog/[slug].html b/test/apps/basics/src/routes/ambiguous/[slug].html
similarity index 100%
rename from test/unit/create_routes/samples/basic/blog/[slug].html
rename to test/apps/basics/src/routes/ambiguous/[slug].html
diff --git a/test/apps/basics/src/routes/ambiguous/[slug].json.js b/test/apps/basics/src/routes/ambiguous/[slug].json.js
new file mode 100644
index 0000000..adc3102
--- /dev/null
+++ b/test/apps/basics/src/routes/ambiguous/[slug].json.js
@@ -0,0 +1,3 @@
+export function get(req, res) {
+ res.end(req.params.slug);
+}
\ No newline at end of file
diff --git a/test/apps/basics/src/routes/b/index.html b/test/apps/basics/src/routes/b/index.html
new file mode 100644
index 0000000..d93079d
--- /dev/null
+++ b/test/apps/basics/src/routes/b/index.html
@@ -0,0 +1,11 @@
+
{letter}
+
+
\ No newline at end of file
diff --git a/test/apps/basics/src/routes/b/index.json.js b/test/apps/basics/src/routes/b/index.json.js
new file mode 100644
index 0000000..29b7ede
--- /dev/null
+++ b/test/apps/basics/src/routes/b/index.json.js
@@ -0,0 +1,3 @@
+export function get(req, res) {
+ res.end(JSON.stringify('b'));
+}
\ No newline at end of file
diff --git a/test/apps/basics/src/routes/const.html b/test/apps/basics/src/routes/const.html
new file mode 100644
index 0000000..6975aae
--- /dev/null
+++ b/test/apps/basics/src/routes/const.html
@@ -0,0 +1 @@
+
reserved words are okay as routes
\ No newline at end of file
diff --git a/test/app/routes/api/delete/[id].js b/test/apps/basics/src/routes/delete-test/[id].json.js
similarity index 100%
rename from test/app/routes/api/delete/[id].js
rename to test/apps/basics/src/routes/delete-test/[id].json.js
diff --git a/test/app/routes/delete-test.html b/test/apps/basics/src/routes/delete-test/index.html
similarity index 67%
rename from test/app/routes/delete-test.html
rename to test/apps/basics/src/routes/delete-test/index.html
index 428ab3c..a5280cb 100644
--- a/test/app/routes/delete-test.html
+++ b/test/apps/basics/src/routes/delete-test/index.html
@@ -2,9 +2,13 @@
\ No newline at end of file
diff --git a/test/apps/css/src/routes/foo.html b/test/apps/css/src/routes/foo.html
new file mode 100644
index 0000000..3aeeeeb
--- /dev/null
+++ b/test/apps/css/src/routes/foo.html
@@ -0,0 +1,7 @@
+
Foo
+
+
\ No newline at end of file
diff --git a/test/apps/css/src/routes/index.html b/test/apps/css/src/routes/index.html
new file mode 100644
index 0000000..a9fd9fd
--- /dev/null
+++ b/test/apps/css/src/routes/index.html
@@ -0,0 +1,10 @@
+
Great success!
+
+foo
+bar
+
+
\ No newline at end of file
diff --git a/test/apps/css/src/server.js b/test/apps/css/src/server.js
new file mode 100644
index 0000000..0e7741c
--- /dev/null
+++ b/test/apps/css/src/server.js
@@ -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);
diff --git a/test/apps/css/src/service-worker.js b/test/apps/css/src/service-worker.js
new file mode 100644
index 0000000..9d2ac9d
--- /dev/null
+++ b/test/apps/css/src/service-worker.js
@@ -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;
+ }
+ })
+ );
+});
diff --git a/test/apps/css/src/template.html b/test/apps/css/src/template.html
new file mode 100644
index 0000000..0eb1f3b
--- /dev/null
+++ b/test/apps/css/src/template.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ %sapper.base%
+ %sapper.styles%
+ %sapper.head%
+
+
+
\ No newline at end of file
diff --git a/test/apps/encoding/src/routes/echo/page/[slug].html b/test/apps/encoding/src/routes/echo/page/[slug].html
new file mode 100644
index 0000000..2cdc03d
--- /dev/null
+++ b/test/apps/encoding/src/routes/echo/page/[slug].html
@@ -0,0 +1,11 @@
+
{slug} {JSON.stringify(query)}
+
+
\ No newline at end of file
diff --git a/test/apps/encoding/src/routes/echo/server-route/[slug].js b/test/apps/encoding/src/routes/echo/server-route/[slug].js
new file mode 100644
index 0000000..e0e9ef7
--- /dev/null
+++ b/test/apps/encoding/src/routes/echo/server-route/[slug].js
@@ -0,0 +1,15 @@
+export function get(req, res) {
+ res.writeHead(200, {
+ 'Content-Type': 'text/html'
+ });
+
+ res.end(`
+
+
+
+
+
${req.params.slug}
+
+
+ `);
+}
\ No newline at end of file
diff --git a/test/apps/encoding/src/routes/fünke.html b/test/apps/encoding/src/routes/fünke.html
new file mode 100644
index 0000000..e1fba6c
--- /dev/null
+++ b/test/apps/encoding/src/routes/fünke.html
@@ -0,0 +1,11 @@
+
{phrase}
+
+
\ No newline at end of file
diff --git a/test/apps/encoding/src/routes/fünke.json.js b/test/apps/encoding/src/routes/fünke.json.js
new file mode 100644
index 0000000..a9ccd6e
--- /dev/null
+++ b/test/apps/encoding/src/routes/fünke.json.js
@@ -0,0 +1,9 @@
+export function get(req, res) {
+ res.writeHead(200, {
+ 'Content-Type': 'application/json'
+ });
+
+ res.end(JSON.stringify(
+ "I'm afraid I just blue myself"
+ ));
+}
\ No newline at end of file
diff --git a/test/apps/encoding/src/routes/index.html b/test/apps/encoding/src/routes/index.html
new file mode 100644
index 0000000..198a277
--- /dev/null
+++ b/test/apps/encoding/src/routes/index.html
@@ -0,0 +1,3 @@
+
Great success!
+
+link
\ No newline at end of file
diff --git a/test/apps/encoding/src/server.js b/test/apps/encoding/src/server.js
new file mode 100644
index 0000000..0e7741c
--- /dev/null
+++ b/test/apps/encoding/src/server.js
@@ -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);
diff --git a/test/apps/encoding/src/service-worker.js b/test/apps/encoding/src/service-worker.js
new file mode 100644
index 0000000..9d2ac9d
--- /dev/null
+++ b/test/apps/encoding/src/service-worker.js
@@ -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;
+ }
+ })
+ );
+});
diff --git a/test/apps/encoding/src/template.html b/test/apps/encoding/src/template.html
new file mode 100644
index 0000000..0eb1f3b
--- /dev/null
+++ b/test/apps/encoding/src/template.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ %sapper.base%
+ %sapper.styles%
+ %sapper.head%
+
+
+
\ No newline at end of file
diff --git a/test/apps/errors/src/routes/async-throw.json.js b/test/apps/errors/src/routes/async-throw.json.js
new file mode 100644
index 0000000..25158f7
--- /dev/null
+++ b/test/apps/errors/src/routes/async-throw.json.js
@@ -0,0 +1,3 @@
+export function get(req, res) {
+ return Promise.reject(new Error('oops'));
+}
\ No newline at end of file
diff --git a/test/apps/errors/src/routes/blog/[slug].html b/test/apps/errors/src/routes/blog/[slug].html
new file mode 100644
index 0000000..8a304d0
--- /dev/null
+++ b/test/apps/errors/src/routes/blog/[slug].html
@@ -0,0 +1,19 @@
+
{post.title}
+
+
\ No newline at end of file
diff --git a/test/apps/errors/src/routes/blog/[slug].json.js b/test/apps/errors/src/routes/blog/[slug].json.js
new file mode 100644
index 0000000..5e2e2a7
--- /dev/null
+++ b/test/apps/errors/src/routes/blog/[slug].json.js
@@ -0,0 +1,9 @@
+export function get(req, res) {
+ res.writeHead(404, {
+ 'Content-Type': 'application/json'
+ });
+
+ res.end(JSON.stringify({
+ message: 'not found'
+ }));
+}
\ No newline at end of file
diff --git a/test/apps/errors/src/routes/enhance-your-calm.html b/test/apps/errors/src/routes/enhance-your-calm.html
new file mode 100644
index 0000000..398584e
--- /dev/null
+++ b/test/apps/errors/src/routes/enhance-your-calm.html
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/test/apps/errors/src/routes/index.html b/test/apps/errors/src/routes/index.html
new file mode 100644
index 0000000..a6b7896
--- /dev/null
+++ b/test/apps/errors/src/routes/index.html
@@ -0,0 +1,5 @@
+
root
+
+nope
+blog/nope
+throw
\ No newline at end of file
diff --git a/test/apps/errors/src/routes/no-error.html b/test/apps/errors/src/routes/no-error.html
new file mode 100644
index 0000000..8dc116a
--- /dev/null
+++ b/test/apps/errors/src/routes/no-error.html
@@ -0,0 +1,3 @@
+
{error ? error.message : 'No error here'}
+
+Enhance your calm
\ No newline at end of file
diff --git a/test/apps/errors/src/routes/nope.json.js b/test/apps/errors/src/routes/nope.json.js
new file mode 100644
index 0000000..3813784
--- /dev/null
+++ b/test/apps/errors/src/routes/nope.json.js
@@ -0,0 +1,4 @@
+export function get(req, res) {
+ res.writeHead(500);
+ res.end('nope');
+}
\ No newline at end of file
diff --git a/test/apps/errors/src/routes/throw.html b/test/apps/errors/src/routes/throw.html
new file mode 100644
index 0000000..16b4131
--- /dev/null
+++ b/test/apps/errors/src/routes/throw.html
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/test/apps/errors/src/routes/throw.json.js b/test/apps/errors/src/routes/throw.json.js
new file mode 100644
index 0000000..5247b57
--- /dev/null
+++ b/test/apps/errors/src/routes/throw.json.js
@@ -0,0 +1,3 @@
+export function get(req, res) {
+ throw new Error('oops');
+}
\ No newline at end of file
diff --git a/test/apps/errors/src/server.js b/test/apps/errors/src/server.js
new file mode 100644
index 0000000..0e7741c
--- /dev/null
+++ b/test/apps/errors/src/server.js
@@ -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);
diff --git a/test/apps/errors/src/service-worker.js b/test/apps/errors/src/service-worker.js
new file mode 100644
index 0000000..9d2ac9d
--- /dev/null
+++ b/test/apps/errors/src/service-worker.js
@@ -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;
+ }
+ })
+ );
+});
diff --git a/test/apps/errors/src/template.html b/test/apps/errors/src/template.html
new file mode 100644
index 0000000..0eb1f3b
--- /dev/null
+++ b/test/apps/errors/src/template.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ %sapper.base%
+ %sapper.styles%
+ %sapper.head%
+
+
+
\ No newline at end of file
diff --git a/test/apps/export/src/routes/blog/[slug].html b/test/apps/export/src/routes/blog/[slug].html
new file mode 100644
index 0000000..f44adaa
--- /dev/null
+++ b/test/apps/export/src/routes/blog/[slug].html
@@ -0,0 +1,11 @@
+
{post.title}
+
+
\ No newline at end of file
diff --git a/test/apps/export/src/routes/blog/[slug].json.js b/test/apps/export/src/routes/blog/[slug].json.js
new file mode 100644
index 0000000..66781ad
--- /dev/null
+++ b/test/apps/export/src/routes/blog/[slug].json.js
@@ -0,0 +1,19 @@
+import posts from './_posts.js';
+
+export function get(req, res) {
+ const post = posts.find(post => post.slug === req.params.slug);
+
+ if (post) {
+ res.writeHead(200, {
+ 'Content-Type': 'application/json'
+ });
+
+ res.end(JSON.stringify(post));
+ } else {
+ res.writeHead(404, {
+ 'Content-Type': 'application/json'
+ });
+
+ res.end(JSON.stringify({ message: 'not found' }));
+ }
+}
\ No newline at end of file
diff --git a/test/apps/export/src/routes/blog/_posts.js b/test/apps/export/src/routes/blog/_posts.js
new file mode 100644
index 0000000..d283aa6
--- /dev/null
+++ b/test/apps/export/src/routes/blog/_posts.js
@@ -0,0 +1,5 @@
+export default [
+ { slug: 'foo', title: 'once upon a foo' },
+ { slug: 'bar', title: 'a bar is born' },
+ { slug: 'baz', title: 'bazzily ever after' }
+];
\ No newline at end of file
diff --git a/test/apps/export/src/routes/blog/index.html b/test/apps/export/src/routes/blog/index.html
new file mode 100644
index 0000000..2eea681
--- /dev/null
+++ b/test/apps/export/src/routes/blog/index.html
@@ -0,0 +1,15 @@
+
\ No newline at end of file
diff --git a/test/apps/ignore/src/routes/_error.html b/test/apps/ignore/src/routes/_error.html
new file mode 100644
index 0000000..4cd55d2
--- /dev/null
+++ b/test/apps/ignore/src/routes/_error.html
@@ -0,0 +1,3 @@
+
{status}
+
+
{error.message}
\ No newline at end of file
diff --git a/test/apps/ignore/src/routes/a.html b/test/apps/ignore/src/routes/a.html
new file mode 100644
index 0000000..0e6757b
--- /dev/null
+++ b/test/apps/ignore/src/routes/a.html
@@ -0,0 +1 @@
+
a
\ No newline at end of file
diff --git a/test/apps/ignore/src/routes/b/index.html b/test/apps/ignore/src/routes/b/index.html
new file mode 100644
index 0000000..d93079d
--- /dev/null
+++ b/test/apps/ignore/src/routes/b/index.html
@@ -0,0 +1,11 @@
+
{letter}
+
+
\ No newline at end of file
diff --git a/test/apps/ignore/src/routes/b/index.json.js b/test/apps/ignore/src/routes/b/index.json.js
new file mode 100644
index 0000000..29b7ede
--- /dev/null
+++ b/test/apps/ignore/src/routes/b/index.json.js
@@ -0,0 +1,3 @@
+export function get(req, res) {
+ res.end(JSON.stringify('b'));
+}
\ No newline at end of file
diff --git a/test/apps/ignore/src/routes/index.html b/test/apps/ignore/src/routes/index.html
new file mode 100644
index 0000000..e08c0cb
--- /dev/null
+++ b/test/apps/ignore/src/routes/index.html
@@ -0,0 +1,3 @@
+
Great success!
+
+a
\ No newline at end of file
diff --git a/test/apps/ignore/src/server.js b/test/apps/ignore/src/server.js
new file mode 100644
index 0000000..f987e4a
--- /dev/null
+++ b/test/apps/ignore/src/server.js
@@ -0,0 +1,19 @@
+import polka from 'polka';
+import * as sapper from '../__sapper__/server.js';
+
+const { PORT } = process.env;
+
+const app = polka().use(sapper.middleware({
+ ignore: [
+ /foobar/i,
+ '/buzz',
+ 'fizz',
+ x => x === '/hello'
+ ]
+}));
+
+['foobar', 'buzz', 'fizzer', 'hello'].forEach(uri => {
+ app.get('/'+uri, (req, res) => res.end(uri));
+});
+
+app.listen(PORT);
diff --git a/test/apps/ignore/src/service-worker.js b/test/apps/ignore/src/service-worker.js
new file mode 100644
index 0000000..9d2ac9d
--- /dev/null
+++ b/test/apps/ignore/src/service-worker.js
@@ -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;
+ }
+ })
+ );
+});
diff --git a/test/apps/ignore/src/template.html b/test/apps/ignore/src/template.html
new file mode 100644
index 0000000..0eb1f3b
--- /dev/null
+++ b/test/apps/ignore/src/template.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ %sapper.base%
+ %sapper.styles%
+ %sapper.head%
+
+
+
\ No newline at end of file
diff --git a/test/app/routes/[x]/[y]/[z].html b/test/apps/layout/src/routes/[x]/[y]/[z].html
similarity index 87%
rename from test/app/routes/[x]/[y]/[z].html
rename to test/apps/layout/src/routes/[x]/[y]/[z].html
index 787e413..018f001 100644
--- a/test/app/routes/[x]/[y]/[z].html
+++ b/test/apps/layout/src/routes/[x]/[y]/[z].html
@@ -1,5 +1,5 @@
z: {segment} {count}
-
+click me
\ No newline at end of file
diff --git a/test/app/routes/redirect-root.html b/test/apps/redirects/src/routes/redirect-to-root.html
similarity index 70%
rename from test/app/routes/redirect-root.html
rename to test/apps/redirects/src/routes/redirect-to-root.html
index ddd4b49..0fa438b 100644
--- a/test/app/routes/redirect-root.html
+++ b/test/apps/redirects/src/routes/redirect-to-root.html
@@ -1,7 +1,9 @@
+
unredirected
+
+
\ No newline at end of file
diff --git a/test/app/routes/redirect-to.html b/test/apps/redirects/src/routes/redirect-to.html
similarity index 100%
rename from test/app/routes/redirect-to.html
rename to test/apps/redirects/src/routes/redirect-to.html
diff --git a/test/apps/redirects/src/server.js b/test/apps/redirects/src/server.js
new file mode 100644
index 0000000..0e7741c
--- /dev/null
+++ b/test/apps/redirects/src/server.js
@@ -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);
diff --git a/test/apps/redirects/src/service-worker.js b/test/apps/redirects/src/service-worker.js
new file mode 100644
index 0000000..9d2ac9d
--- /dev/null
+++ b/test/apps/redirects/src/service-worker.js
@@ -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;
+ }
+ })
+ );
+});
diff --git a/test/apps/redirects/src/template.html b/test/apps/redirects/src/template.html
new file mode 100644
index 0000000..0eb1f3b
--- /dev/null
+++ b/test/apps/redirects/src/template.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ %sapper.base%
+ %sapper.styles%
+ %sapper.head%
+
+
+
%sapper.html%
+ %sapper.scripts%
+
+
diff --git a/test/apps/redirects/test.ts b/test/apps/redirects/test.ts
new file mode 100644
index 0000000..50d6507
--- /dev/null
+++ b/test/apps/redirects/test.ts
@@ -0,0 +1,139 @@
+import * as assert from 'assert';
+import * as puppeteer from 'puppeteer';
+import { build } from '../../../api';
+import { AppRunner } from '../AppRunner';
+import { wait } from '../../utils';
+
+describe('redirects', function() {
+ this.timeout(10000);
+
+ let runner: AppRunner;
+ let page: puppeteer.Page;
+ let base: string;
+
+ // helpers
+ let start: () => Promise;
+ let prefetchRoutes: () => Promise;
+ let title: () => Promise;
+
+ // hooks
+ before(async () => {
+ await build({ cwd: __dirname });
+
+ runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
+ ({ base, page, start, prefetchRoutes, title } = await runner.start({
+ requestInterceptor: (interceptedRequest) => {
+ if (/example\.com/.test(interceptedRequest.url())) {
+ interceptedRequest.respond({
+ status: 200,
+ contentType: 'text/html',
+ body: `
\ No newline at end of file
diff --git a/test/apps/scroll/src/routes/another-tall-page.html b/test/apps/scroll/src/routes/another-tall-page.html
new file mode 100644
index 0000000..d66879b
--- /dev/null
+++ b/test/apps/scroll/src/routes/another-tall-page.html
@@ -0,0 +1,2 @@
+
+
element
\ No newline at end of file
diff --git a/test/apps/scroll/src/routes/index.html b/test/apps/scroll/src/routes/index.html
new file mode 100644
index 0000000..0cc4b72
--- /dev/null
+++ b/test/apps/scroll/src/routes/index.html
@@ -0,0 +1 @@
+
Great success!
\ No newline at end of file
diff --git a/test/apps/scroll/src/routes/tall-page.html b/test/apps/scroll/src/routes/tall-page.html
new file mode 100644
index 0000000..67db967
--- /dev/null
+++ b/test/apps/scroll/src/routes/tall-page.html
@@ -0,0 +1,24 @@
+scroll to foo
+
+
+
+
+
\ No newline at end of file
diff --git a/test/apps/scroll/src/server.js b/test/apps/scroll/src/server.js
new file mode 100644
index 0000000..0e7741c
--- /dev/null
+++ b/test/apps/scroll/src/server.js
@@ -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);
diff --git a/test/apps/scroll/src/service-worker.js b/test/apps/scroll/src/service-worker.js
new file mode 100644
index 0000000..9d2ac9d
--- /dev/null
+++ b/test/apps/scroll/src/service-worker.js
@@ -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;
+ }
+ })
+ );
+});
diff --git a/test/apps/scroll/src/template.html b/test/apps/scroll/src/template.html
new file mode 100644
index 0000000..0eb1f3b
--- /dev/null
+++ b/test/apps/scroll/src/template.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ %sapper.base%
+ %sapper.styles%
+ %sapper.head%
+
+
+
%sapper.html%
+ %sapper.scripts%
+
+
diff --git a/test/apps/scroll/test.ts b/test/apps/scroll/test.ts
new file mode 100644
index 0000000..cd9209e
--- /dev/null
+++ b/test/apps/scroll/test.ts
@@ -0,0 +1,91 @@
+import * as assert from 'assert';
+import * as puppeteer from 'puppeteer';
+import { build } from '../../../api';
+import { AppRunner } from '../AppRunner';
+import { wait } from '../../utils';
+
+describe('scroll', function() {
+ this.timeout(10000);
+
+ let runner: AppRunner;
+ let page: puppeteer.Page;
+ let base: string;
+
+ // helpers
+ let start: () => Promise;
+ let prefetchRoutes: () => Promise;
+
+ // 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('scrolls to active deeplink', async () => {
+ await page.goto(`${base}/tall-page#foo`);
+ await start();
+
+ const scrollY = await page.evaluate(() => window.scrollY);
+ assert.ok(scrollY > 0, scrollY);
+ });
+
+ it('scrolls to any deeplink if it was already active', async () => {
+ await page.goto(`${base}/tall-page#foo`);
+ await start();
+
+ let scrollY = await page.evaluate(() => window.scrollY);
+ assert.ok(scrollY > 0, scrollY);
+
+ scrollY = await page.evaluate(() => {
+ window.scrollTo(0, 0)
+ return window.scrollY
+ });
+ assert.ok(scrollY === 0, scrollY);
+
+ await page.click('[href="tall-page#foo"]');
+ scrollY = await page.evaluate(() => window.scrollY);
+ assert.ok(scrollY > 0, scrollY);
+ });
+
+ it('resets scroll when a link is clicked', async () => {
+ await page.goto(`${base}/tall-page#foo`);
+ await start();
+ await prefetchRoutes();
+
+ await page.click('[href="another-tall-page"]');
+ await wait(50);
+
+ assert.equal(
+ await page.evaluate(() => window.scrollY),
+ 0
+ );
+ });
+
+ it('preserves scroll when a link with sapper-noscroll is clicked', async () => {
+ await page.goto(`${base}/tall-page#foo`);
+ await start();
+ await prefetchRoutes();
+
+ await page.click('[href="another-tall-page"][sapper-noscroll]');
+ await wait(50);
+
+ const scrollY = await page.evaluate(() => window.scrollY);
+
+ assert.ok(scrollY > 0);
+ });
+
+ it('scrolls into a deeplink on a new page', async () => {
+ await page.goto(`${base}/tall-page#foo`);
+ await start();
+ await prefetchRoutes();
+
+ await page.click('[href="another-tall-page#bar"]');
+ await wait(50);
+ const scrollY = await page.evaluate(() => window.scrollY);
+ assert.ok(scrollY > 0);
+ });
+});
\ No newline at end of file
diff --git a/test/apps/store/rollup.config.js b/test/apps/store/rollup.config.js
new file mode 100644
index 0000000..943b676
--- /dev/null
+++ b/test/apps/store/rollup.config.js
@@ -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)
+ })
+ ]
+ }
+};
\ No newline at end of file
diff --git a/test/apps/store/src/client.js b/test/apps/store/src/client.js
new file mode 100644
index 0000000..df66471
--- /dev/null
+++ b/test/apps/store/src/client.js
@@ -0,0 +1,11 @@
+import { Store } from 'svelte/store.js';
+import * as sapper from '../__sapper__/client.js';
+
+window.start = () => sapper.start({
+ target: document.querySelector('#sapper'),
+ store: data => new Store(data)
+});
+
+window.prefetchRoutes = () => sapper.prefetchRoutes();
+window.prefetch = href => sapper.prefetch(href);
+window.goto = href => sapper.goto(href);
\ No newline at end of file
diff --git a/test/apps/store/src/routes/_error.html b/test/apps/store/src/routes/_error.html
new file mode 100644
index 0000000..4cd55d2
--- /dev/null
+++ b/test/apps/store/src/routes/_error.html
@@ -0,0 +1,3 @@
+
{status}
+
+
{error.message}
\ No newline at end of file
diff --git a/test/apps/store/src/routes/index.html b/test/apps/store/src/routes/index.html
new file mode 100644
index 0000000..221c0f5
--- /dev/null
+++ b/test/apps/store/src/routes/index.html
@@ -0,0 +1,7 @@
+
Great success!
+
+a
+ok
+ok
+
+
\ No newline at end of file
diff --git a/test/app/routes/store.html b/test/apps/store/src/routes/store.html
similarity index 100%
rename from test/app/routes/store.html
rename to test/apps/store/src/routes/store.html
diff --git a/test/apps/store/src/server.js b/test/apps/store/src/server.js
new file mode 100644
index 0000000..c40e690
--- /dev/null
+++ b/test/apps/store/src/server.js
@@ -0,0 +1,22 @@
+import polka from 'polka';
+import { Store } from 'svelte/store.js';
+import * as sapper from '../__sapper__/server.js';
+
+const { PORT } = process.env;
+
+polka()
+ .use((req, res, next) => {
+ req.hello = 'hello';
+ res.locals = { name: 'world' };
+ next();
+ })
+ .use(
+ sapper.middleware({
+ store: (req, res) => {
+ return new Store({
+ title: `${req.hello} ${res.locals.name}`
+ });
+ }
+ })
+ )
+ .listen(PORT);
diff --git a/test/apps/store/src/service-worker.js b/test/apps/store/src/service-worker.js
new file mode 100644
index 0000000..9d2ac9d
--- /dev/null
+++ b/test/apps/store/src/service-worker.js
@@ -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;
+ }
+ })
+ );
+});
diff --git a/test/apps/store/src/template.html b/test/apps/store/src/template.html
new file mode 100644
index 0000000..0eb1f3b
--- /dev/null
+++ b/test/apps/store/src/template.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ %sapper.base%
+ %sapper.styles%
+ %sapper.head%
+
+
+
\ No newline at end of file
diff --git a/test/apps/with-basepath/src/routes/index.html b/test/apps/with-basepath/src/routes/index.html
new file mode 100644
index 0000000..0cc4b72
--- /dev/null
+++ b/test/apps/with-basepath/src/routes/index.html
@@ -0,0 +1 @@
+
Great success!
\ No newline at end of file
diff --git a/test/apps/with-basepath/src/server.js b/test/apps/with-basepath/src/server.js
new file mode 100644
index 0000000..b187dad
--- /dev/null
+++ b/test/apps/with-basepath/src/server.js
@@ -0,0 +1,16 @@
+import sirv from 'sirv';
+import polka from 'polka';
+import * as sapper from '../__sapper__/server.js';
+
+const { PORT, NODE_ENV } = process.env;
+const dev = NODE_ENV === 'development';
+
+polka()
+ .use(
+ 'custom-basepath',
+ sirv('static', { dev }),
+ sapper.middleware()
+ )
+ .listen(PORT, err => {
+ if (err) console.log('error', err);
+ });
diff --git a/test/apps/with-basepath/src/service-worker.js b/test/apps/with-basepath/src/service-worker.js
new file mode 100644
index 0000000..9d2ac9d
--- /dev/null
+++ b/test/apps/with-basepath/src/service-worker.js
@@ -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;
+ }
+ })
+ );
+});
diff --git a/test/apps/with-basepath/src/template.html b/test/apps/with-basepath/src/template.html
new file mode 100644
index 0000000..0eb1f3b
--- /dev/null
+++ b/test/apps/with-basepath/src/template.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ %sapper.base%
+ %sapper.styles%
+ %sapper.head%
+
+
+
%sapper.html%
+ %sapper.scripts%
+
+
diff --git a/test/apps/with-basepath/static/global.css b/test/apps/with-basepath/static/global.css
new file mode 100644
index 0000000..800f57a
--- /dev/null
+++ b/test/apps/with-basepath/static/global.css
@@ -0,0 +1,3 @@
+body {
+ font-family: 'Comic Sans MS';
+}
\ No newline at end of file
diff --git a/test/apps/with-basepath/test.ts b/test/apps/with-basepath/test.ts
new file mode 100644
index 0000000..5741d1b
--- /dev/null
+++ b/test/apps/with-basepath/test.ts
@@ -0,0 +1,63 @@
+import * as assert from 'assert';
+import * as puppeteer from 'puppeteer';
+import * as api from '../../../api';
+import { walk } from '../../utils';
+import { AppRunner } from '../AppRunner';
+
+describe('with-basepath', function() {
+ this.timeout(10000);
+
+ let runner: AppRunner;
+ let page: puppeteer.Page;
+ let base: string;
+
+ // hooks
+ before(async () => {
+ await api.build({ cwd: __dirname });
+
+ await api.export({
+ cwd: __dirname,
+ basepath: '/custom-basepath'
+ });
+
+ runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
+ ({ base, page } = await runner.start());
+ });
+
+ after(() => runner.end());
+
+ it('serves /custom-basepath', async () => {
+ await page.goto(`${base}/custom-basepath`);
+
+ assert.equal(
+ await page.$eval('h1', node => node.textContent),
+ 'Great success!'
+ );
+ });
+
+ it('emits a basepath message', async () => {
+ await page.goto(`${base}/custom-basepath`);
+
+ assert.deepEqual(runner.messages, [{
+ __sapper__: true,
+ event: 'basepath',
+ basepath: '/custom-basepath'
+ }]);
+ });
+
+ it('crawls an exported site with basepath', () => {
+ const files = walk(`${__dirname}/__sapper__/export`);
+
+ const client_assets = files.filter(file => file.startsWith('custom-basepath/client/'));
+ const non_client_assets = files.filter(file => !file.startsWith('custom-basepath/client/')).sort();
+
+ assert.ok(client_assets.length > 0);
+
+ assert.deepEqual(non_client_assets, [
+ 'custom-basepath/global.css',
+ 'custom-basepath/index.html',
+ 'custom-basepath/service-worker-index.html',
+ 'custom-basepath/service-worker.js'
+ ]);
+ });
+});
\ No newline at end of file
diff --git a/test/apps/with-sourcemaps-webpack/src/client.js b/test/apps/with-sourcemaps-webpack/src/client.js
new file mode 100644
index 0000000..0865a4a
--- /dev/null
+++ b/test/apps/with-sourcemaps-webpack/src/client.js
@@ -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);
\ No newline at end of file
diff --git a/test/apps/with-sourcemaps-webpack/src/routes/_error.html b/test/apps/with-sourcemaps-webpack/src/routes/_error.html
new file mode 100644
index 0000000..4cd55d2
--- /dev/null
+++ b/test/apps/with-sourcemaps-webpack/src/routes/_error.html
@@ -0,0 +1,3 @@
+
{status}
+
+
{error.message}
\ No newline at end of file
diff --git a/test/apps/with-sourcemaps-webpack/src/routes/index.html b/test/apps/with-sourcemaps-webpack/src/routes/index.html
new file mode 100644
index 0000000..abaff72
--- /dev/null
+++ b/test/apps/with-sourcemaps-webpack/src/routes/index.html
@@ -0,0 +1,3 @@
+
Great success!
+
+
Woot!
\ No newline at end of file
diff --git a/test/apps/with-sourcemaps-webpack/src/server.js b/test/apps/with-sourcemaps-webpack/src/server.js
new file mode 100644
index 0000000..0e7741c
--- /dev/null
+++ b/test/apps/with-sourcemaps-webpack/src/server.js
@@ -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);
diff --git a/test/apps/with-sourcemaps-webpack/src/service-worker.js b/test/apps/with-sourcemaps-webpack/src/service-worker.js
new file mode 100644
index 0000000..9d2ac9d
--- /dev/null
+++ b/test/apps/with-sourcemaps-webpack/src/service-worker.js
@@ -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;
+ }
+ })
+ );
+});
diff --git a/test/apps/with-sourcemaps-webpack/src/template.html b/test/apps/with-sourcemaps-webpack/src/template.html
new file mode 100644
index 0000000..0eb1f3b
--- /dev/null
+++ b/test/apps/with-sourcemaps-webpack/src/template.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ %sapper.base%
+ %sapper.styles%
+ %sapper.head%
+
+
+
\ No newline at end of file
diff --git a/test/apps/with-sourcemaps/src/routes/index.html b/test/apps/with-sourcemaps/src/routes/index.html
new file mode 100644
index 0000000..abaff72
--- /dev/null
+++ b/test/apps/with-sourcemaps/src/routes/index.html
@@ -0,0 +1,3 @@
+
Great success!
+
+
Woot!
\ No newline at end of file
diff --git a/test/apps/with-sourcemaps/src/server.js b/test/apps/with-sourcemaps/src/server.js
new file mode 100644
index 0000000..0e7741c
--- /dev/null
+++ b/test/apps/with-sourcemaps/src/server.js
@@ -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);
diff --git a/test/apps/with-sourcemaps/src/service-worker.js b/test/apps/with-sourcemaps/src/service-worker.js
new file mode 100644
index 0000000..9d2ac9d
--- /dev/null
+++ b/test/apps/with-sourcemaps/src/service-worker.js
@@ -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;
+ }
+ })
+ );
+});
diff --git a/test/apps/with-sourcemaps/src/template.html b/test/apps/with-sourcemaps/src/template.html
new file mode 100644
index 0000000..0eb1f3b
--- /dev/null
+++ b/test/apps/with-sourcemaps/src/template.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ %sapper.base%
+ %sapper.styles%
+ %sapper.head%
+
+
+