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 +
Custom layout
\ 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) =>