diff --git a/packages/kit/.eslintrc.json b/packages/kit/.eslintrc.json
new file mode 100644
index 000000000000..11dde8fbdc5e
--- /dev/null
+++ b/packages/kit/.eslintrc.json
@@ -0,0 +1,7 @@
+{
+ "root": true,
+ "extends": "@sveltejs",
+ "parserOptions": {
+ "sourceType": "module"
+ }
+}
\ No newline at end of file
diff --git a/packages/kit/.gitignore b/packages/kit/.gitignore
index 9f4013ceffe7..0e2363f3cc19 100644
--- a/packages/kit/.gitignore
+++ b/packages/kit/.gitignore
@@ -1,6 +1,6 @@
.DS_Store
/node_modules
/dist
-/assets/app
+/assets/runtime
/assets/renderer
/client/**/*.d.ts
diff --git a/packages/kit/package.json b/packages/kit/package.json
index 61dd81552d4a..cfed59bedf82 100644
--- a/packages/kit/package.json
+++ b/packages/kit/package.json
@@ -17,6 +17,7 @@
"@types/node": "^14.11.10",
"@types/rimraf": "^3.0.0",
"@types/sade": "^1.7.2",
+ "eslint": "^7.14.0",
"esm": "^3.2.25",
"estree-walker": "^2.0.1",
"kleur": "^4.1.3",
@@ -43,7 +44,7 @@
"scripts": {
"dev": "rollup -cw",
"build": "rollup -c",
- "lint": "eslint --ignore-pattern node_modules/ --ignore-pattern dist/ \"**/*.{ts,mjs,js,svelte}\" && npm run check-format",
+ "lint": "eslint --ignore-pattern node_modules/ --ignore-pattern dist/ --ignore-pattern assets/ \"**/*.{ts,mjs,js,svelte}\" && npm run check-format",
"format": "prettier --write . --config ../../.prettierrc --ignore-path .gitignore",
"check-format": "prettier --check . --config ../../.prettierrc --ignore-path .gitignore",
"prepublishOnly": "npm run build",
diff --git a/packages/kit/rollup.config.js b/packages/kit/rollup.config.js
index 9f2d9d9ac725..c0472f11723a 100644
--- a/packages/kit/rollup.config.js
+++ b/packages/kit/rollup.config.js
@@ -12,16 +12,18 @@ const external = [].concat(
export default [
{
input: {
- navigation: 'src/runtime/navigation/index.js',
- stores: 'src/runtime/stores/index.js'
+ 'internal/start': 'src/runtime/internal/start.js',
+ 'internal/singletons': 'src/runtime/internal/singletons.js',
+ 'app/navigation': 'src/runtime/app/navigation/index.js',
+ 'app/stores': 'src/runtime/app/stores/index.js'
},
output: {
- dir: 'assets/app',
+ dir: 'assets/runtime',
format: 'esm',
sourcemap: true,
paths: {
- ROOT: '../generated/root.svelte',
- MANIFEST: '../generated/manifest.js'
+ ROOT: '../../generated/root.svelte',
+ MANIFEST: '../../generated/manifest.js'
}
},
external: ['svelte', 'svelte/store', 'ROOT', 'MANIFEST'],
@@ -69,4 +71,4 @@ export default [
],
preserveEntrySignatures: true
}
-];
\ No newline at end of file
+];
diff --git a/packages/kit/src/api/build/index.js b/packages/kit/src/api/build/index.js
index 0e26cac84701..2fb97df1b6e9 100644
--- a/packages/kit/src/api/build/index.js
+++ b/packages/kit/src/api/build/index.js
@@ -67,7 +67,7 @@ export async function build(config) {
fs.writeFileSync(setup_file, '');
}
- const mount = `--mount.${config.paths.routes}=/_app/routes --mount.${config.paths.setup}=/_app/setup`
+ const mount = `--mount.${config.paths.routes}=/_app/routes --mount.${config.paths.setup}=/_app/setup`;
await exec(`node ${snowpack_bin} build ${mount} --out=${unoptimized}/server --ssr`);
log.success('server');
@@ -87,7 +87,7 @@ export async function build(config) {
deps: {}
};
- const entry = path.resolve(`${unoptimized}/client/_app/assets/app/navigation.js`);
+ const entry = path.resolve(`${unoptimized}/client/_app/assets/runtime/internal/start.js`);
// https://github.com/snowpackjs/snowpack/discussions/1395
const re = /(\.\.\/)+_app\/assets\/app\//;
diff --git a/packages/kit/src/api/dev/index.js b/packages/kit/src/api/dev/index.js
index 667f33b6570e..46bdbe56ac66 100644
--- a/packages/kit/src/api/dev/index.js
+++ b/packages/kit/src/api/dev/index.js
@@ -139,7 +139,7 @@ class Watcher extends EventEmitter {
root = (await load('/_app/assets/generated/root.js')).default;
} catch (e) {
res.statusCode = 500;
- res.end(e.toString());
+ res.end(e.stack);
return;
}
@@ -162,7 +162,7 @@ class Watcher extends EventEmitter {
manifest: this.manifest,
target: this.config.target,
client: {
- entry: 'assets/app/navigation.js',
+ entry: 'assets/runtime/internal/start.js',
deps: {}
},
dev: true,
diff --git a/packages/kit/src/api/dev/loader.js b/packages/kit/src/api/dev/loader.js
index d07e2b08fb73..dd46617eb9bc 100644
--- a/packages/kit/src/api/dev/loader.js
+++ b/packages/kit/src/api/dev/loader.js
@@ -160,7 +160,7 @@ export default function loader(snowpack, config) {
if (node.type === 'MetaProperty' && node.meta.name === 'import') {
code.overwrite(node.start, node.end, '__importmeta__');
} else if (node.type === 'ImportExpression') {
- code.overwrite(node.start, node.start + 6, `__import__`);
+ code.overwrite(node.start, node.start + 6, '__import__');
}
}
});
diff --git a/packages/kit/src/api/index.js b/packages/kit/src/api/index.js
index 6047755520f2..fc504eb68e8c 100644
--- a/packages/kit/src/api/index.js
+++ b/packages/kit/src/api/index.js
@@ -1,4 +1,4 @@
export { dev } from './dev';
export { build } from './build';
export { start } from './start';
-export { load_config } from './load_config';
\ No newline at end of file
+export { load_config } from './load_config';
diff --git a/packages/kit/src/api/load_config/index.js b/packages/kit/src/api/load_config/index.js
index 0c935659cfae..902195ac0ff0 100644
--- a/packages/kit/src/api/load_config/index.js
+++ b/packages/kit/src/api/load_config/index.js
@@ -21,4 +21,4 @@ export function load_config({ cwd = process.cwd() } = {}) {
...config.paths
}
};
-}
\ No newline at end of file
+}
diff --git a/packages/kit/src/api/start/index.js b/packages/kit/src/api/start/index.js
index ea850a2f6701..599f7bf81c20 100644
--- a/packages/kit/src/api/start/index.js
+++ b/packages/kit/src/api/start/index.js
@@ -53,4 +53,4 @@ export function start({ port }) {
fulfil(server);
});
});
-}
\ No newline at end of file
+}
diff --git a/packages/kit/src/api/utils.js b/packages/kit/src/api/utils.js
index 9730600d8b12..3ad3357a981d 100644
--- a/packages/kit/src/api/utils.js
+++ b/packages/kit/src/api/utils.js
@@ -2,5 +2,5 @@ import { resolve } from 'path';
import { copy } from '@sveltejs/app-utils/files';
export function copy_assets() {
- copy(resolve(__dirname, `../assets`), '.svelte/assets');
+ copy(resolve(__dirname, '../assets'), '.svelte/assets');
}
diff --git a/packages/kit/src/core/create_app.js b/packages/kit/src/core/create_app.js
index a90c5e926894..4b8e3b4fb1e3 100644
--- a/packages/kit/src/core/create_app.js
+++ b/packages/kit/src/core/create_app.js
@@ -47,12 +47,6 @@ export function create_serviceworker_manifest({
write_if_changed(`${output}/service-worker.js`, code);
}
-function create_param_match(param, i) {
- return /^\.{3}.+$/.test(param)
- ? `${param.replace(/.{3}/, '')}: d(match[${i + 1}]).split('/')`
- : `${param}: d(match[${i + 1}])`;
-}
-
function generate_client_manifest(manifest_data) {
const page_ids = new Set(manifest_data.pages.map(page =>
page.pattern.toString()));
@@ -74,49 +68,52 @@ function generate_client_manifest(manifest_data) {
let needs_decode = false;
- let routes = `[
- ${manifest_data.pages
- .map(
- (page) => `{
+ let pages = `[
+ ${manifest_data.pages
+ .map(
+ (page) => `{
// ${page.parts[page.parts.length - 1].component.file}
pattern: ${page.pattern},
parts: [
${page.parts
.map((part) => {
const missing_layout = !part;
- if (missing_layout) return 'null';
+ if (missing_layout) return null;
if (part.params.length > 0) {
needs_decode = true;
- const props = part.params.map(create_param_match);
- return `{ i: ${
- component_indexes[part.component.name]
- }, params: match => ({ ${props.join(', ')} }) }`;
+ const props = part.params.map((param, i) => {
+ return param.startsWith('...')
+ ? `${param.slice(3)}: d(m[${i + 1}]).split('/')`
+ : `${param}: d(m[${i + 1}])`;
+ });
+ return `[components[${component_indexes[part.component.name]}], m => ({ ${props.join(', ')} })]`;
}
- return `{ i: ${component_indexes[part.component.name]} }`;
+ return `[components[${component_indexes[part.component.name]}]]`;
})
- .join(',\n\t\t\t\t\t\t')}
+ .filter(Boolean)
+ .join(',\n\t\t\t\t')}
]
- }`
- )
- .join(',\n\n\t\t\t\t')}
+ }`).join(',\n\n\t\t')}
]`.replace(/^\t/gm, '');
if (needs_decode) {
- routes = `(d => ${routes})(decodeURIComponent)`;
+ pages = `(d => ${pages})(decodeURIComponent)`;
}
return `
import * as layout from ${JSON.stringify(manifest_data.layout.url)};
- export { layout };
- export { default as ErrorComponent } from ${JSON.stringify(manifest_data.error.url)};
- export const ignore = [${endpoints_to_ignore.map(route => route.pattern).join(', ')}];
+ const components = ${components};
- export const components = ${components};
+ export const pages = ${pages};
- export const routes = ${routes};
+ export const ignore = [
+ ${endpoints_to_ignore.map(route => route.pattern).join(',\n\t\t\t')}
+ ];
+
+ export { layout };
`
.replace(/^\t{2}/gm, '')
.trim();
@@ -130,18 +127,18 @@ function generate_app(manifest_data) {
);
const levels = [];
- for (let i = 0; i < max_depth; i += 1) {
- levels.push(i + 1);
+ for (let i = 0; i <= max_depth; i += 1) {
+ levels.push(i);
}
let l = max_depth;
- let pyramid = ``;
+ let pyramid = ``;
while (l-- > 1) {
pyramid = `
-
- {#if level${l + 1}}
+
+ {#if components[${l + 1}]}
${pyramid.replace(/\n/g, '\n\t\t\t\t\t')}
{/if}
@@ -154,25 +151,28 @@ function generate_app(manifest_data) {
-
+
{#if error}
{:else}
diff --git a/packages/kit/src/core/create_manifest_data.js b/packages/kit/src/core/create_manifest_data.js
index 3be0da64b6e9..7ba2329ab781 100644
--- a/packages/kit/src/core/create_manifest_data.js
+++ b/packages/kit/src/core/create_manifest_data.js
@@ -128,7 +128,7 @@ export default function create_manifest_data(
path.join(dir, item.basename),
segments,
params,
- component ? stack.concat({ component, params }) : stack.concat(null)
+ component ? stack.concat({ component, params }) : stack
);
} else if (item.is_page) {
const component = {
diff --git a/packages/kit/src/core/test/create_manifest_data.spec.js b/packages/kit/src/core/test/create_manifest_data.spec.js
index 29105c4dedc0..36b3526551e6 100644
--- a/packages/kit/src/core/test/create_manifest_data.spec.js
+++ b/packages/kit/src/core/test/create_manifest_data.spec.js
@@ -47,7 +47,6 @@ test('creates routes', () => {
path: null,
pattern: /^\/blog\/([^/]+?)\/?$/,
parts: [
- null,
{ component: blog_$slug, params: ['slug'] }
]
}
@@ -119,16 +118,16 @@ test('sorts routes correctly', () => {
['index.svelte'],
['about.svelte'],
['post/index.svelte'],
- [null, 'post/bar.svelte'],
- [null, 'post/foo.svelte'],
- [null, 'post/f[xx].svelte'],
- [null, 'post/[id([0-9-a-z]{3,})].svelte'],
- [null, 'post/[id].svelte'],
+ ['post/bar.svelte'],
+ ['post/foo.svelte'],
+ ['post/f[xx].svelte'],
+ ['post/[id([0-9-a-z]{3,})].svelte'],
+ ['post/[id].svelte'],
['[wildcard].svelte'],
- [null, null, null, '[...spread]/deep/[...deep_spread]/xyz.svelte'],
- [null, null, '[...spread]/deep/[...deep_spread]/index.svelte'],
- [null, '[...spread]/deep/index.svelte'],
- [null, '[...spread]/abc.svelte'],
+ ['[...spread]/deep/[...deep_spread]/xyz.svelte'],
+ ['[...spread]/deep/[...deep_spread]/index.svelte'],
+ ['[...spread]/deep/index.svelte'],
+ ['[...spread]/abc.svelte'],
['[...spread]/index.svelte']
]);
});
@@ -184,8 +183,7 @@ test('fails on clashes', () => {
}, /The \[bar\]\/index\.svelte and \[foo\]\.svelte pages clash/);
assert.throws(() => {
- const { server_routes } = create_manifest_data(path.join(__dirname, 'samples/clash-routes'));
- console.log(server_routes);
+ create_manifest_data(path.join(__dirname, 'samples/clash-routes'));
}, /The \[bar\]\/index\.js and \[foo\]\.js routes clash/);
});
@@ -257,7 +255,6 @@ test('works with custom extensions' , () => {
path: null,
pattern: /^\/blog\/([^/]+?)\/?$/,
parts: [
- null,
{ component: blog_$slug, params: ['slug'] }
]
}
@@ -281,4 +278,4 @@ test('works with custom extensions' , () => {
]);
});
-test.run();
\ No newline at end of file
+test.run();
diff --git a/packages/kit/src/renderer/page.js b/packages/kit/src/renderer/page.js
index e43aa6aac54d..32d9cc7ccbf9 100644
--- a/packages/kit/src/renderer/page.js
+++ b/packages/kit/src/renderer/page.js
@@ -2,12 +2,10 @@
import { createReadStream, existsSync } from 'fs';
import * as mime from 'mime';
import fetch, { Response } from 'node-fetch';
-import { readable, writable } from 'svelte/store';
+import { writable } from 'svelte/store';
import { parse, resolve, URLSearchParams } from 'url';
import { render } from './index';
-const noop = () => {};
-
async function get_response({
request,
options,
@@ -18,9 +16,7 @@ async function get_response({
}) {
let redirected;
- const segments = request.path.split('/').filter(Boolean);
-
- const baseUrl = ''; // TODO
+ const base = ''; // TODO
const dependencies = {};
@@ -136,18 +132,16 @@ async function get_response({
// these are only the parameters up to the current URL segment
const params = parts_to_params(match, part);
- const props = mod.preload
- ? await mod.preload.call(
- preload_context,
- {
- host: request.host,
- path: request.path,
- query: request.query,
- params
- },
- session
- )
- : {};
+ const props = mod.preload ? await mod.preload.call(
+ preload_context,
+ {
+ host: request.host,
+ path: request.path,
+ query: request.query,
+ params
+ },
+ session
+ ) : {};
preloaded[i] = props;
return { component: mod.default, props };
@@ -158,63 +152,35 @@ async function get_response({
if (redirected) return redirected;
- // TODO make this less confusing
- const layout_segments = [segments[0]];
- let l = 1;
-
- if (page) {
- page.parts.forEach((part, i) => {
- layout_segments[l] = segments[i + 1];
- if (!part) return;
- l++;
- });
- }
-
const props = {
status,
error,
stores: {
- page: readable({
- host: request.host,
- path: request.path,
- query: request.query,
- params,
- error
- }, noop),
- preloading: readable(null, noop),
+ page: writable(null),
+ preloading: writable(false),
session: writable(session)
},
- // TODO stores, status, segments, notify, CONTEXT_KEY
- segments: layout_segments,
- level0: {
- props: preloaded[0]
+ page: {
+ host: request.host,
+ path: request.path,
+ query: request.query,
+ params,
+ error
},
- level1: {
- segment: segments[0],
- props: {}
- }
+ components: parts.map(part => part.component)
};
// leveln (instead of levels[n]) makes it easy to avoid
// unnecessary updates for layout components
- l = 1;
- for (let i = 1; i < parts.length; i += 1) {
- const part = parts[i];
- if (!part) continue;
-
- props[`level${l++}`] = {
- component: part.component,
- props: preloaded[i] || {},
- segment: segments[i]
- };
- }
+ parts.forEach((part, i) => {
+ props[`props_${i}`] = part.props;
+ });
const serialized_preloads = `[${preloaded
.map((data) =>
try_serialize(data, (err) => {
- const path = '/' + segments.join('/');
console.error(
- `Failed to serialize preloaded data to transmit to the client at the ${path} route: ${err.message}`
+ `Failed to serialize preloaded data to transmit to the client at the ${request.path} route: ${err.message}`
);
console.warn(
'The client will re-render over the server-rendered page fresh instead of continuing where it left off. See https://sapper.svelte.dev/docs#Return_value for more information'
@@ -257,7 +223,7 @@ async function get_response({
import { start } from '/_app/${options.client.entry}';
start({
target: ${options.target ? `document.querySelector(${JSON.stringify(options.target)})` : 'document.body'},
- baseUrl: "${baseUrl}",
+ base: "${base}",
status: ${status},
error: ${serialize_error(error)},
preloaded: ${serialized_preloads},
diff --git a/packages/kit/src/runtime/app/navigation/index.js b/packages/kit/src/runtime/app/navigation/index.js
new file mode 100644
index 000000000000..8bcea06b98a8
--- /dev/null
+++ b/packages/kit/src/runtime/app/navigation/index.js
@@ -0,0 +1,44 @@
+import { router, renderer } from '../../internal/singletons';
+
+function get_base_uri(window_document) {
+ let baseURI = window_document.baseURI;
+
+ if (!baseURI) {
+ const baseTags = window_document.getElementsByTagName('base');
+ baseURI = baseTags.length ? baseTags[0].href : window_document.URL;
+ }
+
+ return baseURI;
+}
+
+export function goto(href, { noscroll = false, replaceState = false } = {}) {
+ const url = new URL(href, get_base_uri(document));
+ const page = router.select(url);
+
+ if (page) {
+ // TODO this logic probably belongs inside router? cid should be private
+ history[replaceState ? 'replaceState' : 'pushState']({ id: router.cid }, '', href);
+
+ // TODO shouldn't need to pass the hash here
+ return router.navigate(page, null, noscroll, url.hash);
+ }
+
+ location.href = href;
+ return new Promise(() => {
+ /* never resolves */
+ });
+}
+
+export function prefetch(href) {
+ return renderer.prefetch(new URL(href, get_base_uri(document)));
+}
+
+export async function prefetchRoutes(pathnames) {
+ const path_routes = pathnames
+ ? router.pages.filter((page) => pathnames.some((pathname) => page.pattern.test(pathname)))
+ : router.pages;
+
+ const promises = path_routes.map((r) => Promise.all(r.parts.map((p) => p[0]())));
+
+ await Promise.all(promises);
+}
diff --git a/packages/kit/src/runtime/stores/index.js b/packages/kit/src/runtime/app/stores/index.js
similarity index 78%
rename from packages/kit/src/runtime/stores/index.js
rename to packages/kit/src/runtime/app/stores/index.js
index 3abf1d61c46d..64982b7c95d3 100644
--- a/packages/kit/src/runtime/stores/index.js
+++ b/packages/kit/src/runtime/app/stores/index.js
@@ -3,7 +3,19 @@ import { getContext } from 'svelte';
// const ssr = (import.meta as any).env.SSR;
const ssr = typeof window === 'undefined'; // TODO why doesn't previous line work in build?
-export const getStores = () => getContext('__svelte__');
+export const getStores = () => {
+ const stores = getContext('__svelte__');
+
+ return {
+ page: {
+ subscribe: stores.page.subscribe
+ },
+ preloading: {
+ subscribe: stores.preloading.subscribe
+ },
+ session: stores.session
+ };
+};
export const page = {
subscribe(fn) {
diff --git a/packages/kit/src/runtime/internal/renderer/index.js b/packages/kit/src/runtime/internal/renderer/index.js
new file mode 100644
index 000000000000..77b5440ec76a
--- /dev/null
+++ b/packages/kit/src/runtime/internal/renderer/index.js
@@ -0,0 +1,268 @@
+import { writable } from 'svelte/store';
+import { find_anchor } from '../utils';
+
+function page_store(value) {
+ const store = writable(value);
+ let ready = true;
+
+ function notify() {
+ ready = true;
+ store.update((val) => val);
+ }
+
+ function set(new_value) {
+ ready = false;
+ store.set(new_value);
+ }
+
+ function subscribe(run) {
+ let old_value;
+ return store.subscribe((new_value) => {
+ if (old_value === undefined || (ready && new_value !== old_value)) {
+ run((old_value = new_value));
+ }
+ });
+ }
+
+ return { notify, set, subscribe };
+}
+
+export class Renderer {
+ constructor({
+ Root,
+ layout,
+ target,
+ error,
+ status,
+ preloaded,
+ session
+ }) {
+ this.Root = Root;
+ this.layout = layout;
+ this.layout_loader = () => layout;
+
+ // TODO ideally we wouldn't need to store these...
+ this.target = target;
+
+ this.initial = {
+ preloaded,
+ error,
+ status
+ };
+
+ this.current_branch = [];
+
+ this.prefetching = {
+ href: null,
+ promise: null
+ };
+
+ this.stores = {
+ page: page_store({}),
+ preloading: writable(false),
+ session: writable(session)
+ };
+
+ this.$session = null;
+ this.session_dirty = false;
+
+ this.root = null;
+
+ const trigger_prefetch = (event) => {
+ const a = find_anchor(event.target);
+
+ if (a && a.rel === 'prefetch') { // TODO make this svelte-prefetch or something
+ this.prefetch(new URL(a.href));
+ }
+ };
+
+ let mousemove_timeout;
+ const handle_mousemove = (event) => {
+ clearTimeout(mousemove_timeout);
+ mousemove_timeout = setTimeout(() => {
+ trigger_prefetch(event);
+ }, 20);
+ };
+
+ addEventListener('touchstart', trigger_prefetch);
+ addEventListener('mousemove', handle_mousemove);
+
+ let ready = false;
+ this.stores.session.subscribe(async (value) => {
+ this.$session = value;
+
+ if (!ready) return;
+ this.session_dirty = true;
+
+ const page = this.router.select(new URL(location.href));
+ this.render(page);
+ });
+ ready = true;
+ }
+
+ async start(page) {
+ const props = {
+ stores: this.stores,
+ error: this.initial.error,
+ status: this.initial.status
+ };
+
+ if (!this.initial.error) {
+ const hydrated = await this.hydrate(page);
+
+ if (hydrated.redirect) {
+ throw new Error('TODO client-side redirects');
+ }
+
+ Object.assign(props, hydrated.props);
+ this.current_branch = hydrated.branch;
+ this.current_query = hydrated.query;
+ this.current_path = hydrated.path;
+ }
+
+ this.root = new this.Root({
+ target: this.target,
+ props,
+ hydrate: true
+ });
+
+ this.initial = null;
+ }
+
+ async render(page) {
+ const token = this.token = {};
+
+ this.stores.preloading.set(true);
+
+ const hydrated = await this.hydrate(page);
+
+ if (this.token === token) { // check render wasn't aborted
+ this.current_branch = hydrated.branch;
+ this.current_query = hydrated.query;
+ this.current_path = hydrated.path;
+
+ this.root.$set(hydrated.props);
+
+ this.stores.preloading.set(false);
+ }
+ }
+
+ async hydrate({ route, page }) {
+ let redirect = null;
+
+ const props = {
+ error: null,
+ status: 200,
+ components: []
+ };
+
+ const preload_context = {
+ fetch: (url, opts) => fetch(url, opts),
+ redirect: (status, location) => {
+ if (redirect && (redirect.status !== status || redirect.location !== location)) {
+ throw new Error('Conflicting redirects');
+ }
+ redirect = { status, location };
+ },
+ error: (status, error) => {
+ props.error = typeof error === 'string' ? new Error(error) : error;
+ props.status = status;
+ }
+ };
+
+ const query = page.query.toString();
+ const query_dirty = query !== this.current_query;
+
+ let branch;
+
+ try {
+ const match = route.pattern.exec(page.path);
+
+ branch = await Promise.all(
+ [[this.layout_loader], ...route.parts].map(async ([loader, get_params], i) => {
+ const params = get_params ? get_params(match) : {};
+ const stringified_params = JSON.stringify(params);
+
+ const previous = this.current_branch[i];
+ if (previous) {
+ const changed = (
+ (previous.loader !== loader) ||
+ (previous.uses_session && this.session_dirty) ||
+ (previous.uses_query && query_dirty) ||
+ (previous.stringified_params !== stringified_params)
+ );
+
+ if (!changed) {
+ props.components[i] = previous.component;
+ return previous;
+ }
+ }
+
+ const { default: component, preload } = await loader();
+
+ const uses_session = preload && preload.length > 1;
+ let uses_query = false;
+
+ const preloaded = this.initial?.preloaded[i] || (
+ preload
+ ? await preload.call(
+ preload_context,
+ {
+ get query() {
+ uses_query = true;
+ return page.query;
+ },
+ host: page.host,
+ path: page.path,
+ params
+ },
+ this.$session
+ )
+ : {}
+ );
+
+ props.components[i] = component;
+ props[`props_${i}`] = preloaded;
+
+ return {
+ component,
+ params,
+ stringified_params,
+ props: preloaded,
+ match,
+ loader,
+ uses_session,
+ uses_query
+ };
+ })
+ );
+
+ if (page.path !== this.current_path) {
+ props.page = {
+ ...page,
+ params: branch[branch.length - 1].params
+ };
+ }
+ } catch (error) {
+ props.error = error;
+ props.status = 500;
+ branch = [];
+ }
+
+ return { redirect, props, branch, query, path: page.path };
+ }
+
+ async prefetch(url) {
+ const page = this.router.select(url);
+
+ if (page) {
+ if (url.href !== this.prefetching.href) {
+ this.prefetching = { href: url.href, promise: this.hydrate(page) };
+ }
+
+ return this.prefetching.promise;
+ } else {
+ throw new Error(`Could not prefetch ${url.href}`);
+ }
+ }
+}
diff --git a/packages/kit/src/runtime/internal/router/index.js b/packages/kit/src/runtime/internal/router/index.js
new file mode 100644
index 000000000000..0888a0cac7ef
--- /dev/null
+++ b/packages/kit/src/runtime/internal/router/index.js
@@ -0,0 +1,202 @@
+import { find_anchor } from '../utils';
+
+function which(event) {
+ return event.which === null ? event.button : event.which;
+}
+
+function scroll_state() {
+ return {
+ x: pageXOffset,
+ y: pageYOffset
+ };
+}
+
+export class Router {
+ constructor({ base, pages, ignore }) {
+ this.base = base;
+ this.pages = pages;
+ this.ignore = ignore;
+
+ this.uid = 1;
+ this.cid = null;
+ this.scroll_history = {};
+
+ this.history = window.history || {
+ pushState: () => {},
+ replaceState: () => {},
+ scrollRestoration: 'auto'
+ };
+ }
+
+ init({ renderer }) {
+ this.renderer = renderer;
+ renderer.router = this;
+
+ if ('scrollRestoration' in this.history) {
+ this.history.scrollRestoration = 'manual';
+ }
+
+ // Adopted from Nuxt.js
+ // Reset scrollRestoration to auto when leaving page, allowing page reload
+ // and back-navigation from other pages to use the browser to restore the
+ // scrolling position.
+ addEventListener('beforeunload', () => {
+ this.history.scrollRestoration = 'auto';
+ });
+
+ // Setting scrollRestoration to manual again when returning to this page.
+ addEventListener('load', () => {
+ this.history.scrollRestoration = 'manual';
+ });
+
+ addEventListener('click', event => {
+ // Adapted from https://github.com/visionmedia/page.js
+ // MIT license https://github.com/visionmedia/page.js#license
+ if (which(event) !== 1) return;
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
+ if (event.defaultPrevented) return;
+
+ const a = find_anchor(event.target);
+ if (!a) return;
+
+ if (!a.href) return;
+
+ // check if link is inside an svg
+ // in this case, both href and target are always inside an object
+ const svg = typeof a.href === 'object' && a.href.constructor.name === 'SVGAnimatedString';
+ const href = String(svg ? a.href.baseVal : a.href);
+
+ if (href === location.href) {
+ if (!location.hash) event.preventDefault();
+ return;
+ }
+
+ // Ignore if tag has
+ // 1. 'download' attribute
+ // 2. rel='external' attribute
+ if (a.hasAttribute('download') || a.getAttribute('rel') === 'external') return;
+
+ // Ignore if has a target
+ if (svg ? (a).target.baseVal : a.target) return;
+
+ const url = new URL(href);
+
+ // Don't handle hash changes
+ if (url.pathname === location.pathname && url.search === location.search) return;
+
+ const page = this.select(url);
+ if (page) {
+ const noscroll = a.hasAttribute('sapper:noscroll');
+ this.navigate(page, null, noscroll, url.hash);
+ event.preventDefault();
+ this.history.pushState({ id: this.cid }, '', url.href);
+ }
+ });
+
+ addEventListener('popstate', event => {
+ this.scroll_history[this.cid] = scroll_state();
+
+ if (event.state) {
+ const url = new URL(location.href);
+ const page = this.select(url);
+ if (page) {
+ this.navigate(page, event.state.id);
+ } else {
+ // eslint-disable-next-line
+ location.href = location.href; // nosonar
+ }
+ } else {
+ // hashchange
+ this.uid += 1;
+ this.cid = this.uid;
+ this.history.replaceState({ id: this.cid }, '', location.href);
+ }
+ });
+
+ // load current page
+ this.history.replaceState({ id: this.uid }, '', location.href);
+ this.scroll_history[this.uid] = scroll_state();
+
+ const page = this.select(new URL(location.href));
+ // if (page) return this.navigate(page, this.uid, true, hash);
+ if (page) return this.renderer.start(page);
+ }
+
+ select(url) {
+ if (url.origin !== location.origin) return null;
+ if (!url.pathname.startsWith(this.base)) return null;
+
+ let path = url.pathname.slice(this.base.length);
+
+ if (path === '') {
+ path = '/';
+ }
+
+ // avoid accidental clashes between server routes and page routes
+ if (this.ignore.some(pattern => pattern.test(path))) return;
+
+ for (const route of this.pages) {
+ const match = route.pattern.exec(path);
+
+ if (match) {
+ const query = new URLSearchParams(url.search);
+ const part = route.parts[route.parts.length - 1];
+ const params = part.params ? part.params(match) : {};
+
+ const page = { host: location.host, path, query, params };
+
+ return { href: url.href, route, match, page };
+ }
+ }
+ }
+
+ async navigate(
+ page,
+ id,
+ noscroll,
+ hash
+ ) {
+ const popstate = !!id;
+ if (popstate) {
+ this.cid = id;
+ } else {
+ const current_scroll = scroll_state();
+
+ // clicked on a link. preserve scroll state
+ this.scroll_history[this.cid] = current_scroll;
+
+ this.cid = id = ++this.uid;
+ this.scroll_history[this.cid] = noscroll ? current_scroll : { x: 0, y: 0 };
+ }
+
+ await this.renderer.render(page);
+
+ if (document.activeElement instanceof HTMLElement) {
+ document.activeElement.blur();
+ }
+
+ if (!noscroll) {
+ let scroll = this.scroll_history[id];
+
+ let deep_linked;
+ if (hash) {
+ // scroll is an element id (from a hash), we need to compute y.
+ deep_linked = document.getElementById(hash.slice(1));
+
+ if (deep_linked) {
+ scroll = {
+ x: 0,
+ y: deep_linked.getBoundingClientRect().top + scrollY
+ };
+ }
+ }
+
+ this.scroll_history[this.cid] = scroll;
+ if (popstate || deep_linked) {
+ scrollTo(scroll.x, scroll.y);
+ } else {
+ scrollTo(0, 0);
+ }
+ }
+ }
+}
diff --git a/packages/kit/src/runtime/internal/singletons.js b/packages/kit/src/runtime/internal/singletons.js
new file mode 100644
index 000000000000..454c334888cb
--- /dev/null
+++ b/packages/kit/src/runtime/internal/singletons.js
@@ -0,0 +1,7 @@
+export let router;
+export let renderer;
+
+export function init(opts) {
+ router = opts.router;
+ renderer = opts.renderer;
+}
diff --git a/packages/kit/src/runtime/internal/start.js b/packages/kit/src/runtime/internal/start.js
new file mode 100644
index 000000000000..97b0bda2c780
--- /dev/null
+++ b/packages/kit/src/runtime/internal/start.js
@@ -0,0 +1,34 @@
+import Root from 'ROOT';
+import { pages, ignore, layout } from 'MANIFEST';
+import { Router } from './router';
+import { Renderer } from './renderer';
+import { init } from './singletons';
+
+export async function start({
+ base,
+ target,
+ session,
+ preloaded,
+ error,
+ status
+}) {
+ const router = new Router({
+ base,
+ pages,
+ ignore
+ });
+
+ const renderer = new Renderer({
+ Root,
+ layout,
+ target,
+ preloaded,
+ error,
+ status,
+ session
+ });
+
+ init({ router, renderer });
+
+ await router.init({ renderer });
+}
diff --git a/packages/kit/src/runtime/internal/utils.js b/packages/kit/src/runtime/internal/utils.js
new file mode 100644
index 000000000000..c88eb542a17f
--- /dev/null
+++ b/packages/kit/src/runtime/internal/utils.js
@@ -0,0 +1,4 @@
+export function find_anchor(node) {
+ while (node && node.nodeName.toUpperCase() !== 'A') node = node.parentNode; // SVG elements have a lowercase name
+ return node;
+}
diff --git a/packages/kit/src/runtime/navigation/goto/index.js b/packages/kit/src/runtime/navigation/goto/index.js
deleted file mode 100644
index f8f4de01ce65..000000000000
--- a/packages/kit/src/runtime/navigation/goto/index.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import { cid, history, navigate, select_target } from '../internal';
-import { get_base_uri } from '../utils';
-
-export default function goto(
- href,
- opts = { noscroll: false, replaceState: false }
-) {
- const target = select_target(new URL(href, get_base_uri(document)));
-
- if (target) {
- history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
- return navigate(target, null, opts.noscroll);
- }
-
- location.href = href;
- return new Promise(() => {
- /* never resolves */
- });
-}
diff --git a/packages/kit/src/runtime/navigation/index.js b/packages/kit/src/runtime/navigation/index.js
deleted file mode 100644
index fb2ab4f8b033..000000000000
--- a/packages/kit/src/runtime/navigation/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export { default as goto } from './goto';
-export { default as prefetch } from './prefetch';
-export { default as prefetchRoutes } from './prefetchRoutes';
-export { default as start } from './start';
diff --git a/packages/kit/src/runtime/navigation/internal.js b/packages/kit/src/runtime/navigation/internal.js
deleted file mode 100644
index dc65ffea3c2e..000000000000
--- a/packages/kit/src/runtime/navigation/internal.js
+++ /dev/null
@@ -1,215 +0,0 @@
-import { find_anchor } from './utils';
-import { ignore, routes } from 'MANIFEST';
-
-export let uid = 1;
-export function set_uid(n) {
- uid = n;
-}
-
-export let cid;
-export function set_cid(n) {
- cid = n;
-}
-
-const _history =
- typeof history !== 'undefined'
- ? history
- : {
- pushState: () => {},
- replaceState: () => {},
- scrollRestoration: 'auto'
- };
-export { _history as history };
-
-export const scroll_history = {};
-
-export async function load_current_page() {
- const { hash, href } = location;
-
- _history.replaceState({ id: uid }, '', href);
-
- const target = select_target(new URL(location.href));
- if (target) return navigate(target, uid, true, hash);
-}
-
-let base_url;
-let handle_target;
-
-export function init(base, handler) {
- base_url = base;
- handle_target = handler;
-
- if ('scrollRestoration' in _history) {
- _history.scrollRestoration = 'manual';
- }
-
- // Adopted from Nuxt.js
- // Reset scrollRestoration to auto when leaving page, allowing page reload
- // and back-navigation from other pages to use the browser to restore the
- // scrolling position.
- addEventListener('beforeunload', () => {
- _history.scrollRestoration = 'auto';
- });
-
- // Setting scrollRestoration to manual again when returning to this page.
- addEventListener('load', () => {
- _history.scrollRestoration = 'manual';
- });
-
- addEventListener('click', handle_click);
- addEventListener('popstate', handle_popstate);
-}
-
-export function select_target(url) {
- if (url.origin !== location.origin) return null;
- if (!url.pathname.startsWith(base_url)) return null;
-
- let path = url.pathname.slice(base_url.length);
-
- if (path === '') {
- path = '/';
- }
-
- // avoid accidental clashes between server routes and page routes
- if (ignore.some(pattern => pattern.test(path))) return;
-
- for (let i = 0; i < routes.length; i += 1) {
- const route = routes[i];
-
- const match = route.pattern.exec(path);
-
- if (match) {
- const query = new URLSearchParams(url.search);
- const part = route.parts[route.parts.length - 1];
- const params = part.params ? part.params(match) : {};
-
- const page = { host: location.host, path, query, params };
-
- return { href: url.href, route, match, page };
- }
- }
-}
-
-function handle_click(event) {
- // Adapted from https://github.com/visionmedia/page.js
- // MIT license https://github.com/visionmedia/page.js#license
- if (which(event) !== 1) return;
- if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
- if (event.defaultPrevented) return;
-
- const a = find_anchor(event.target);
- if (!a) return;
-
- if (!a.href) return;
-
- // check if link is inside an svg
- // in this case, both href and target are always inside an object
- const svg = typeof a.href === 'object' && a.href.constructor.name === 'SVGAnimatedString';
- const href = String(svg ? a.href.baseVal : a.href);
-
- if (href === location.href) {
- if (!location.hash) event.preventDefault();
- return;
- }
-
- // Ignore if tag has
- // 1. 'download' attribute
- // 2. rel='external' attribute
- if (a.hasAttribute('download') || a.getAttribute('rel') === 'external') return;
-
- // Ignore if has a target
- if (svg ? (a).target.baseVal : a.target) return;
-
- const url = new URL(href);
-
- // Don't handle hash changes
- if (url.pathname === location.pathname && url.search === location.search) return;
-
- const target = select_target(url);
- if (target) {
- const noscroll = a.hasAttribute('sapper:noscroll');
- navigate(target, null, noscroll, url.hash);
- event.preventDefault();
- _history.pushState({ id: cid }, '', url.href);
- }
-}
-
-function which(event) {
- return event.which === null ? event.button : event.which;
-}
-
-function scroll_state() {
- return {
- x: pageXOffset,
- y: pageYOffset
- };
-}
-
-function handle_popstate(event) {
- scroll_history[cid] = scroll_state();
-
- if (event.state) {
- const url = new URL(location.href);
- const target = select_target(url);
- if (target) {
- navigate(target, event.state.id);
- } else {
- // eslint-disable-next-line
- location.href = location.href; // nosonar
- }
- } else {
- // hashchange
- set_uid(uid + 1);
- set_cid(uid);
- _history.replaceState({ id: cid }, '', location.href);
- }
-}
-
-export async function navigate(
- dest,
- id,
- noscroll,
- hash
-) {
- const popstate = !!id;
- if (popstate) {
- cid = id;
- } else {
- const current_scroll = scroll_state();
-
- // clicked on a link. preserve scroll state
- scroll_history[cid] = current_scroll;
-
- cid = id = ++uid;
- scroll_history[cid] = noscroll ? current_scroll : { x: 0, y: 0 };
- }
-
- await handle_target(dest);
- if (document.activeElement && document.activeElement instanceof HTMLElement) {
- document.activeElement.blur();
- }
-
- if (!noscroll) {
- let scroll = scroll_history[id];
-
- let deep_linked;
- if (hash) {
- // scroll is an element id (from a hash), we need to compute y.
- deep_linked = document.getElementById(hash.slice(1));
-
- if (deep_linked) {
- scroll = {
- x: 0,
- y: deep_linked.getBoundingClientRect().top + scrollY
- };
- }
- }
-
- scroll_history[cid] = scroll;
- if (popstate || deep_linked) {
- scrollTo(scroll.x, scroll.y);
- } else {
- scrollTo(0, 0);
- }
- }
-}
diff --git a/packages/kit/src/runtime/navigation/prefetch/index.js b/packages/kit/src/runtime/navigation/prefetch/index.js
deleted file mode 100644
index d6192b1dbb8e..000000000000
--- a/packages/kit/src/runtime/navigation/prefetch/index.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import { hydrate_target } from '../start'; // TODO does this belong here?
-import { select_target } from '../internal';
-import { find_anchor, get_base_uri } from '../utils';
-
-let prefetching = null;
-
-let mousemove_timeout;
-
-export function start() {
- addEventListener('touchstart', trigger_prefetch);
- addEventListener('mousemove', handle_mousemove);
-}
-
-export default function prefetch(href) {
- const target = select_target(new URL(href, get_base_uri(document)));
-
- if (target) {
- if (!prefetching || href !== prefetching.href) {
- prefetching = { href, promise: hydrate_target(target) };
- }
-
- return prefetching.promise;
- }
-}
-
-export function get_prefetched(target) {
- if (prefetching && prefetching.href === target.href) {
- return prefetching.promise;
- } else {
- return hydrate_target(target);
- }
-}
-
-function trigger_prefetch(event) {
- const a = find_anchor(event.target);
-
- if (a && a.rel === 'prefetch') {
- prefetch(a.href);
- }
-}
-
-function handle_mousemove(event) {
- clearTimeout(mousemove_timeout);
- mousemove_timeout = setTimeout(() => {
- trigger_prefetch(event);
- }, 20);
-}
diff --git a/packages/kit/src/runtime/navigation/prefetchRoutes/index.js b/packages/kit/src/runtime/navigation/prefetchRoutes/index.js
deleted file mode 100644
index ee6243379d4c..000000000000
--- a/packages/kit/src/runtime/navigation/prefetchRoutes/index.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import { components, routes } from 'MANIFEST';
-
-export default async function prefetchRoutes(pathnames) {
- const path_routes = pathnames
- ? routes.filter((route) => pathnames.some((pathname) => route.pattern.test(pathname)))
- : routes;
-
- const promises = path_routes.map((r) => Promise.all(r.parts.map((p) => p && components[p.i]())));
-
- await Promise.all(promises);
-}
diff --git a/packages/kit/src/runtime/navigation/start/index.js b/packages/kit/src/runtime/navigation/start/index.js
deleted file mode 100644
index 64eb63703cf3..000000000000
--- a/packages/kit/src/runtime/navigation/start/index.js
+++ /dev/null
@@ -1,262 +0,0 @@
-import { writable } from 'svelte/store';
-import { init as init_router, load_current_page, select_target } from '../internal';
-import { get_prefetched, start as start_prefetching } from '../prefetch';
-import goto from '../goto';
-import { page_store } from './page_store';
-import { layout, ErrorComponent, components } from 'MANIFEST';
-import root from 'ROOT';
-
-let ready = false;
-let root_component;
-let current_token;
-let initial_preloaded_data;
-let root_preloaded;
-let current_branch = [];
-let current_query = '{}';
-
-const stores = {
- page: page_store({}),
- preloading: writable(false),
- session: writable(null)
-};
-
-let $session;
-let session_dirty;
-
-stores.session.subscribe(async (value) => {
- $session = value;
-
- if (!ready) return;
- session_dirty = true;
-
- const dest = select_target(new URL(location.href));
-
- const token = (current_token = {});
- const { redirect, props, branch } = await hydrate_target(dest);
- if (token !== current_token) return; // a secondary navigation happened while we were loading
-
- if (redirect) {
- await goto(redirect.location, { replaceState: true });
- } else {
- await render(branch, props, buildPageContext(props, dest.page));
- }
-});
-
-export let target;
-export function set_target(node) {
- target = node;
-}
-
-export default async function start(opts) {
- set_target(opts.target);
-
- init_router(opts.baseUrl, handle_target);
-
- start_prefetching();
-
- initial_preloaded_data = opts.preloaded;
- root_preloaded = initial_preloaded_data[0];
-
- stores.session.set(opts.session);
-
- if (opts.error) {
- return handle_error(opts);
- }
-
- return load_current_page();
-}
-
-function handle_error({ session, preloaded, status, error }) {
- const { host, pathname, search } = location;
-
- const props = {
- error,
- status,
- session,
- level0: {
- props: root_preloaded
- },
- level1: {
- props: {
- status,
- error
- },
- component: ErrorComponent
- },
- segments: preloaded
- };
- const query = new URLSearchParams(search);
- render([], props, { host, path: pathname, query, params: {}, error });
-}
-
-function buildPageContext(props, page) {
- const { error } = props;
-
- return { error, ...page };
-}
-
-async function handle_target(dest) {
- if (root_component) stores.preloading.set(true);
-
- const hydrating = get_prefetched(dest);
-
- const token = (current_token = {});
- const hydrated_target = await hydrating;
- const { redirect } = hydrated_target;
- if (token !== current_token) return; // a secondary navigation happened while we were loading
-
- if (redirect) {
- await goto(redirect.location, { replaceState: true });
- } else {
- const { props, branch } = hydrated_target;
- await render(branch, props, buildPageContext(props, dest.page));
- }
-}
-
-async function render(branch, props, page) {
- stores.page.set(page);
- stores.preloading.set(false);
-
- if (root_component) {
- root_component.$set(props);
- } else {
- props.stores = {
- page: { subscribe: stores.page.subscribe },
- preloading: { subscribe: stores.preloading.subscribe },
- session: stores.session
- };
- props.level0 = {
- props: await root_preloaded
- };
- props.notify = stores.page.notify;
-
- root_component = new root({
- target,
- props,
- hydrate: true
- });
- }
-
- current_branch = branch;
- current_query = JSON.stringify(page.query); // TODO this is no good — URLSearchParams can't be serialized like that
- ready = true;
- session_dirty = false;
-}
-
-function part_changed(i, segment, match, stringified_query) {
- // TODO only check query string changes for preload functions
- // that do in fact depend on it (using static analysis or
- // runtime instrumentation)
- if (stringified_query !== current_query) return true;
-
- const previous = current_branch[i];
-
- if (!previous) return false;
- if (segment !== previous.segment) return true;
- if (previous.match) {
- if (JSON.stringify(previous.match.slice(1, i + 2)) !== JSON.stringify(match.slice(1, i + 2))) {
- return true;
- }
- }
-}
-
-export async function hydrate_target(dest) {
- const { route, page } = dest;
- const segments = page.path.split('/').filter(Boolean);
-
- let redirect = null;
-
- const props = { error: null, status: 200, segments: [segments[0]] };
-
- const preload_context = {
- fetch: (url, opts) => fetch(url, opts),
- redirect: (statusCode, location) => {
- if (redirect && (redirect.statusCode !== statusCode || redirect.location !== location)) {
- throw new Error('Conflicting redirects');
- }
- redirect = { statusCode, location };
- },
- error: (status, error) => {
- props.error = typeof error === 'string' ? new Error(error) : error;
- props.status = status;
- }
- };
-
- if (!root_preloaded) {
- root_preloaded =
- (layout.preload
- ? layout.preload.call(
- preload_context,
- {
- host: page.host,
- path: page.path,
- query: page.query,
- params: {}
- },
- $session
- )
- : {});
- }
-
- let branch;
- let l = 1;
-
- try {
- const stringified_query = JSON.stringify(page.query);
- const match = route.pattern.exec(page.path);
-
- let segment_dirty = false;
-
- branch = await Promise.all(
- route.parts.map(async (part, i) => {
- const segment = segments[i];
-
- if (part_changed(i, segment, match, stringified_query)) segment_dirty = true;
-
- props.segments[l] = segments[i + 1]; // TODO make this less confusing
- if (!part) return { segment };
-
- const j = l++;
-
- if (
- !session_dirty &&
- !segment_dirty &&
- current_branch[i] &&
- current_branch[i].part === part.i
- ) {
- return current_branch[i];
- }
-
- segment_dirty = false;
-
- const { default: component, preload } = await components[part.i]();
-
- let preloaded;
- if (ready || !initial_preloaded_data[i + 1]) {
- preloaded = preload
- ? await preload.call(
- preload_context,
- {
- host: page.host,
- path: page.path,
- query: page.query,
- params: part.params ? part.params(dest.match) : {}
- },
- $session
- )
- : {};
- } else {
- preloaded = initial_preloaded_data[i + 1];
- }
-
- return (props[`level${j}`] = { component, props: preloaded, segment, match, part: part.i });
- })
- );
- } catch (error) {
- props.error = error;
- props.status = 500;
- branch = [];
- }
-
- return { redirect, props, branch };
-}
diff --git a/packages/kit/src/runtime/navigation/start/page_store.js b/packages/kit/src/runtime/navigation/start/page_store.js
deleted file mode 100644
index 9df1822c244a..000000000000
--- a/packages/kit/src/runtime/navigation/start/page_store.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import { writable } from 'svelte/store';
-
-/** Callback to inform of a value updates. */
-export function page_store(value) {
- const store = writable(value);
- let ready = true;
-
- function notify() {
- ready = true;
- store.update((val) => val);
- }
-
- function set(new_value) {
- ready = false;
- store.set(new_value);
- }
-
- function subscribe(run) {
- let old_value;
- return store.subscribe((new_value) => {
- if (old_value === undefined || (ready && new_value !== old_value)) {
- run((old_value = new_value));
- }
- });
- }
-
- return { notify, set, subscribe };
-}
diff --git a/packages/kit/src/runtime/navigation/utils.js b/packages/kit/src/runtime/navigation/utils.js
deleted file mode 100644
index 9d783122a9ee..000000000000
--- a/packages/kit/src/runtime/navigation/utils.js
+++ /dev/null
@@ -1,15 +0,0 @@
-export function get_base_uri(window_document) {
- let baseURI = window_document.baseURI;
-
- if (!baseURI) {
- const baseTags = window_document.getElementsByTagName('base');
- baseURI = baseTags.length ? baseTags[0].href : window_document.URL;
- }
-
- return baseURI;
-}
-
-export function find_anchor(node) {
- while (node && node.nodeName.toUpperCase() !== 'A') node = node.parentNode; // SVG elements have a lowercase name
- return node;
-}
diff --git a/packages/snowpack-config/snowpack.config.js b/packages/snowpack-config/snowpack.config.js
index d3290f1ebcd6..8c9a1366af87 100644
--- a/packages/snowpack-config/snowpack.config.js
+++ b/packages/snowpack-config/snowpack.config.js
@@ -25,6 +25,6 @@ module.exports = {
'.svelte/assets': '/_app/assets'
},
alias: {
- $app: './.svelte/assets/app'
+ $app: './.svelte/assets/runtime/app'
}
};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index be89eda95fe8..5594056d507d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -169,6 +169,7 @@ importers:
'@types/node': 14.11.10
'@types/rimraf': 3.0.0
'@types/sade': 1.7.2
+ eslint: 7.14.0
esm: 3.2.25
estree-walker: 2.0.1
kleur: 4.1.3
@@ -189,6 +190,7 @@ importers:
'@types/rimraf': ^3.0.0
'@types/sade': ^1.7.2
cheap-watch: ^1.0.2
+ eslint: ^7.14.0
esm: ^3.2.25
estree-walker: ^2.0.1
http-proxy: ^1.18.1
@@ -461,6 +463,23 @@ packages:
node: ^10.12.0 || >=12.0.0
resolution:
integrity: sha512-4YVwPkANLeNtRjMekzux1ci8hIaH5eGKktGqR0d3LWsKNn5B2X/1Z6Trxy7jQXl9EBGE6Yj02O+t09FMeRllaA==
+ /@eslint/eslintrc/0.2.1:
+ dependencies:
+ ajv: 6.12.6
+ debug: 4.3.1
+ espree: 7.3.0
+ globals: 12.4.0
+ ignore: 4.0.6
+ import-fresh: 3.2.2
+ js-yaml: 3.14.0
+ lodash: 4.17.20
+ minimatch: 3.0.4
+ strip-json-comments: 3.1.1
+ dev: true
+ engines:
+ node: ^10.12.0 || >=12.0.0
+ resolution:
+ integrity: sha512-XRUeBZ5zBWLYgSANMpThFddrZZkEbGHgUdt5UJjZfnlN9BGCiUBrf+nvbRupSjMvqzwnQN0qwCmOxITt1cfywA==
/@manypkg/find-root/1.1.0:
dependencies:
'@babel/runtime': 7.12.1
@@ -1768,6 +1787,51 @@ packages:
hasBin: true
resolution:
integrity: sha512-G9+qtYVCHaDi1ZuWzBsOWo2wSwd70TXnU6UHA3cTYHp7gCTXZcpggWFoUVAMRarg68qtPoNfFbzPh+VdOgmwmw==
+ /eslint/7.14.0:
+ dependencies:
+ '@babel/code-frame': 7.10.4
+ '@eslint/eslintrc': 0.2.1
+ ajv: 6.12.6
+ chalk: 4.1.0
+ cross-spawn: 7.0.3
+ debug: 4.3.1
+ doctrine: 3.0.0
+ enquirer: 2.3.6
+ eslint-scope: 5.1.1
+ eslint-utils: 2.1.0
+ eslint-visitor-keys: 2.0.0
+ espree: 7.3.0
+ esquery: 1.3.1
+ esutils: 2.0.3
+ file-entry-cache: 5.0.1
+ functional-red-black-tree: 1.0.1
+ glob-parent: 5.1.1
+ globals: 12.4.0
+ ignore: 4.0.6
+ import-fresh: 3.2.2
+ imurmurhash: 0.1.4
+ is-glob: 4.0.1
+ js-yaml: 3.14.0
+ json-stable-stringify-without-jsonify: 1.0.1
+ levn: 0.4.1
+ lodash: 4.17.20
+ minimatch: 3.0.4
+ natural-compare: 1.4.0
+ optionator: 0.9.1
+ progress: 2.0.3
+ regexpp: 3.1.0
+ semver: 7.3.4
+ strip-ansi: 6.0.0
+ strip-json-comments: 3.1.1
+ table: 5.4.6
+ text-table: 0.2.0
+ v8-compile-cache: 2.2.0
+ dev: true
+ engines:
+ node: ^10.12.0 || >=12.0.0
+ hasBin: true
+ resolution:
+ integrity: sha512-5YubdnPXrlrYAFCKybPuHIAH++PINe1pmKNc5wQRB9HSbqIK1ywAnntE3Wwua4giKu0bjligf1gLF6qxMGOYRA==
/esm/3.2.25:
dev: true
engines:
@@ -2268,6 +2332,15 @@ packages:
node: '>=6'
resolution:
integrity: sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==
+ /import-fresh/3.2.2:
+ dependencies:
+ parent-module: 1.0.1
+ resolve-from: 4.0.0
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-cTPNrlvJT6twpYy+YmKUKrTSjWFs3bjYjAhCwm+z4EOCubZxAuO+hHpRN64TqjEaYSHs7tJAE0w1CKMGmsG/lw==
/imurmurhash/0.1.4:
engines:
node: '>=0.8.19'
@@ -2604,7 +2677,6 @@ packages:
/lru-cache/6.0.0:
dependencies:
yallist: 4.0.0
- dev: false
engines:
node: '>=10'
resolution:
@@ -3555,6 +3627,15 @@ packages:
hasBin: true
resolution:
integrity: sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
+ /semver/7.3.4:
+ dependencies:
+ lru-cache: 6.0.0
+ dev: true
+ engines:
+ node: '>=10'
+ hasBin: true
+ resolution:
+ integrity: sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==
/serialize-javascript/4.0.0:
dependencies:
randombytes: 2.1.0
@@ -4114,6 +4195,10 @@ packages:
dev: true
resolution:
integrity: sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==
+ /v8-compile-cache/2.2.0:
+ dev: true
+ resolution:
+ integrity: sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==
/validate-npm-package-license/3.0.4:
dependencies:
spdx-correct: 3.1.1
@@ -4233,7 +4318,6 @@ packages:
resolution:
integrity: sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
/yallist/4.0.0:
- dev: false
resolution:
integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
/yaml/1.10.0:
diff --git a/test/apps/basics/src/routes/$layout.svelte b/test/apps/basics/src/routes/$layout.svelte
index 23ffdc3c9dca..793b485cb309 100644
--- a/test/apps/basics/src/routes/$layout.svelte
+++ b/test/apps/basics/src/routes/$layout.svelte
@@ -1,11 +1,11 @@
-
+
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/test/apps/basics/src/routes/middleware/index.js b/test/apps/basics/src/routes/middleware/index_.js
similarity index 100%
rename from test/apps/basics/src/routes/middleware/index.js
rename to test/apps/basics/src/routes/middleware/index_.js
diff --git a/test/apps/basics/src/routes/routing/[...rest]/index.svelte b/test/apps/basics/src/routes/routing/[...rest]/index.svelte
index dbe813c3a083..af2c76315ed7 100644
--- a/test/apps/basics/src/routes/routing/[...rest]/index.svelte
+++ b/test/apps/basics/src/routes/routing/[...rest]/index.svelte
@@ -1,5 +1,5 @@
Test
Called {call_count} time
results
+
+{#if $page.path === '/store/result'}
+ {console.log(window.oops = 'this should not happen')}
+{/if}
\ No newline at end of file
diff --git a/test/runner.js b/test/runner.js
index 32f26daa3fe2..db0b586938d8 100644
--- a/test/runner.js
+++ b/test/runner.js
@@ -8,7 +8,7 @@ import * as assert from 'uvu/assert';
async function setup({ port }) {
const browser = await chromium.launch();
const page = await browser.newPage();
- const defaultTimeout = 2000;
+ const defaultTimeout = 500;
const text = async (selector) => page.textContent(selector, { timeout: defaultTimeout });
const wait_for_text = async (selector, expectedValue) => {
@@ -43,6 +43,7 @@ async function setup({ port }) {
return {
base,
+ page,
visit: path => page.goto(base + path),
contains: async str => (await page.innerHTML('body')).includes(str),
html: async selector => await page.innerHTML(selector, { timeout: defaultTimeout }),
@@ -52,7 +53,7 @@ async function setup({ port }) {
// these are assumed to have been put in the global scope by the layout
goto: (url) => page.evaluate((url) => goto(url), url),
prefetch: (url) => page.evaluate((url) => prefetch(url), url),
- click: (selector, options) => page.click(selector, options),
+ click: (selector, options) => page.click(selector, { timeout: defaultTimeout, ...options }),
prefetch_routes: () => page.evaluate(() => prefetchRoutes()),
wait_for_text,
wait_for_selector: (selector, options) =>