Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] application versioning and update detection support #3412

Merged
merged 40 commits into from
Feb 1, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
c11c4f1
Version Packages (next)
github-actions[bot] Jan 31, 2022
e0cfda6
Remove optional function and add "reload" option to router's onError …
pzerelles Jan 31, 2022
612a7b6
Add app version and only reload if navigation fails and version changed
pzerelles Jan 31, 2022
061863d
Merge branch 'master' into route-error-handling
Rich-Harris Feb 1, 2022
859ea09
Remove unused code
pzerelles Feb 1, 2022
36fce15
Update packages/kit/src/core/build/build_client.js
Rich-Harris Feb 1, 2022
cc68467
Update packages/kit/src/core/build/build_client.js
Rich-Harris Feb 1, 2022
05fb7d5
Make version and check interval configurable
pzerelles Feb 1, 2022
629920b
Restore original router configuration without error option
pzerelles Feb 1, 2022
64fa844
Fix instantiation of "updated" store
pzerelles Feb 1, 2022
eebe181
Merge remote-tracking branch 'upstream/master' into route-error-handling
pzerelles Feb 1, 2022
0e6d7e1
Merge remote-tracking branch 'origin/route-error-handling' into route…
pzerelles Feb 1, 2022
ec09520
Fix pnpm-lock.yaml
pzerelles Feb 1, 2022
344b32a
Update packages/kit/src/core/build/build_client.js
pzerelles Feb 1, 2022
f122617
Update packages/kit/src/runtime/app/env.js
pzerelles Feb 1, 2022
7263f32
Update packages/kit/src/core/config/options.js
pzerelles Feb 1, 2022
603bc06
Patch tests to work with version timestamp
pzerelles Feb 1, 2022
32b7a5f
Update packages/kit/src/core/adapt/prerender/fixtures/large-page/inpu…
Rich-Harris Feb 1, 2022
d8733ef
Update packages/kit/src/core/adapt/prerender/fixtures/large-page/outp…
Rich-Harris Feb 1, 2022
c0506d3
Update packages/kit/src/core/adapt/prerender/fixtures/large-page/inpu…
Rich-Harris Feb 1, 2022
6db21b7
Remove version from $app/env
pzerelles Feb 1, 2022
c6788b5
Apply suggestions from code review
pzerelles Feb 1, 2022
5483f58
Use default version poll interval to 0 (don't poll)
pzerelles Feb 1, 2022
4c0a744
Only poll if interval is a positive number
pzerelles Feb 1, 2022
05b431a
Merge remote-tracking branch 'upstream/master' into route-error-handling
pzerelles Feb 1, 2022
9174109
Better documentation of "updated" store
pzerelles Feb 1, 2022
ff1a930
Apply suggestions from code review
pzerelles Feb 1, 2022
75e4de5
Update documentation/docs/14-configuration.md
pzerelles Feb 1, 2022
0ee276b
reuse updated store
Rich-Harris Feb 1, 2022
17367b0
return false
Rich-Harris Feb 1, 2022
cf01a3b
always set poll interval
Rich-Harris Feb 1, 2022
f5055f5
lint
Rich-Harris Feb 1, 2022
042033e
make updated store stop polling once change is detected
Rich-Harris Feb 1, 2022
8b63b6b
merge master
Rich-Harris Feb 1, 2022
78aad47
use VITE_SVELTEKIT_ namespace
Rich-Harris Feb 1, 2022
d9631dc
Update documentation/docs/14-configuration.md
Rich-Harris Feb 1, 2022
731e31e
Update documentation/docs/14-configuration.md
Rich-Harris Feb 1, 2022
120cc19
oops
Rich-Harris Feb 1, 2022
844f858
Merge branch 'route-error-handling' of github.com:pz-mxu/sveltekit in…
Rich-Harris Feb 1, 2022
ede1b40
changeset
Rich-Harris Feb 1, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions documentation/docs/05-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ The stores themselves attach to the correct context at the point of subscription
- `page` contains an object with the current [`url`](https://developer.mozilla.org/en-US/docs/Web/API/URL), [`params`](#loading-input-params), [`stuff`](#loading-output-stuff), [`status`](#loading-output-status) and [`error`](#loading-output-error).
- `session` is a [writable store](https://svelte.dev/tutorial/writable-stores) whose initial value is whatever was returned from [`getSession`](#hooks-getsession). It can be written to, but this will _not_ cause changes to persist on the server — this is something you must implement yourself.

- `updated` is a [writable store](https://svelte.dev/tutorial/writable-stores) whose initial value is false. The app's version will be checked every minute for changes, setting the value to true if the version does not match the version at the time the store was created. It can be written to when custom logic is required to detect updates. There is a `check` function to force the version check immediately, returning the result.
pzerelles marked this conversation as resolved.
Show resolved Hide resolved

### $lib

This is a simple alias to `src/lib`, or whatever directory is specified as [`config.kit.files.lib`]. It allows you to access common components and utility modules without `../../../../` nonsense.
Expand Down
5 changes: 4 additions & 1 deletion documentation/docs/14-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ const config = {
appDir: '_app',
browser: {
hydrate: true,
router: true
router: {
enabled: true,
onError: 'fail'
},
},
csp: {
mode: 'auto',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1732,7 +1732,7 @@ <h3 id="modules-$app-stores">$app/stores</h3>
<ul>
<li>
<code>getStores</code> is a convenience function around <code>getContext</code> that
returns <code>{ navigating, page, session }</code>. This needs to be called at the
returns <code>{ navigating, page, session, updating }</code>. This needs to be called at the
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved
top-level or synchronously during component or page initialisation.
</li>
</ul>
Expand Down Expand Up @@ -1786,6 +1786,16 @@ <h3 id="modules-$app-stores">$app/stores</h3>
>. It can be written to, but this will <em>not</em> cause changes to persist on the
server — this is something you must implement yourself.
</li>
<li>
<code>updating</code> is a
<a
href="https://svelte.dev/tutorial/writable-stores"
target="_blank"
rel="noopener noreferrer"
>writable store</a
>
whose initial value is false. The app's version will be checked every minute for changes, setting the value to true if the version does not match the version at the time the store was created. It can be written to when custom logic is required to detect updates. There is a <code>check</code> function to force the version check immediately, returning the result.
</li>
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved
</ul>
<h3 id="modules-$lib">$lib</h3>
<p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
"#loading-output-stuff",
"https://svelte.dev/tutorial/writable-stores",
"#hooks-getsession",
"https://svelte.dev/tutorial/writable-stores",
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved
"#service-workers",
"#configuration",
"#configuration",
Expand Down
8 changes: 8 additions & 0 deletions packages/kit/src/core/build/build_client.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export async function build_client({
output_dir,
client_entry_file
}) {
if (!process.env.VITE_APP_VERSION) process.env.VITE_APP_VERSION = new Date().toISOString();
pzerelles marked this conversation as resolved.
Show resolved Hide resolved

create_app({
manifest_data,
output: `${SVELTE_KIT}/generated`,
Expand Down Expand Up @@ -105,6 +107,12 @@ export async function build_client({
const entry_css = new Set();
find_deps(entry, vite_manifest, entry_js, entry_css);

fs.writeFileSync(
`${client_out_dir}/version.json`,
JSON.stringify(process.env.VITE_APP_VERSION),
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved
'utf-8'
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved
);

return {
assets,
chunks,
Expand Down
5 changes: 4 additions & 1 deletion packages/kit/src/core/build/build_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,10 @@ export class App {
read,
root,
service_worker: ${has_service_worker ? "base + '/service-worker.js'" : 'null'},
router: ${s(config.kit.browser.router)},
router: {
enabled: ${s(config.kit.browser.router.enabled)},
onError: ${s(config.kit.browser.router.onError)}
},
target: ${s(config.kit.target)},
template,
template_contains_nonce: ${template.includes('%svelte.nonce%')},
Expand Down
5 changes: 4 additions & 1 deletion packages/kit/src/core/config/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ const get_defaults = (prefix = '') => ({
appDir: '_app',
browser: {
hydrate: true,
router: true
router: {
enabled: true,
onError: 'fail'
}
},
csp: {
mode: 'auto',
Expand Down
8 changes: 7 additions & 1 deletion packages/kit/src/core/config/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,13 @@ const options = object(

browser: object({
hydrate: boolean(true),
router: boolean(true)
router: object({
enabled: boolean(true),
onError: validate('fail', (input, keypath) => {
if (['reload', 'fail'].includes(input)) return input;
throw new Error(`${keypath} should be either "reload" or "fail"`);
})
})
}),

csp: object({
Expand Down
4 changes: 4 additions & 0 deletions packages/kit/src/runtime/app/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,9 @@ export const mode = import.meta.env.MODE;
* @type {import('$app/env').amp}
*/
export const amp = !!import.meta.env.VITE_SVELTEKIT_AMP;
/**
* @type {import('$app/env').version}
*/
export const version = import.meta.env.VITE_APP_VERSION;

export { prerendering } from '../env.js';
21 changes: 20 additions & 1 deletion packages/kit/src/runtime/app/stores.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export const getStores = () => {
subscribe: stores.navigating.subscribe
};
},
session: stores.session
session: stores.session,
updated: stores.updated
};
};

Expand Down Expand Up @@ -77,3 +78,21 @@ export const session = {
set: () => throw_error('set'),
update: () => throw_error('update')
};

/** @type {typeof import('$app/stores').updated} */
export const updated = {
subscribe(fn) {
const store = getStores().updated;

if (browser) {
updated.set = store.set;
updated.update = store.update;
pzerelles marked this conversation as resolved.
Show resolved Hide resolved
updated.check = store.check;
}

return store.subscribe(fn);
},
set: () => throw_error('set'),
update: () => throw_error('update'),
pzerelles marked this conversation as resolved.
Show resolved Hide resolved
check: () => throw_error('check')
};
62 changes: 59 additions & 3 deletions packages/kit/src/runtime/client/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { normalize } from '../load.js';

/**
* @typedef {import('types/internal').CSRComponent} CSRComponent
* @typedef {import('types/config').RouteOnErrorValue} RouteOnErrorValue
* @typedef {{ from: URL; to: URL }} Navigating
*/

Expand Down Expand Up @@ -39,6 +40,44 @@ function notifiable_store(value) {
return { notify, set, subscribe };
}

/**
* @param {any} value
* @param {(set: (new_value: any) => void) => any} fn
* @param {number | undefined} interval
*/
function checkable_store(value, fn, interval) {
const { set, update, subscribe } = writable(value);

setInterval(() => {
pzerelles marked this conversation as resolved.
Show resolved Hide resolved
fn(set);
}, interval);

return {
set,
update,
pzerelles marked this conversation as resolved.
Show resolved Hide resolved
subscribe,
check: () => fn(set)
};
}

/**
* @returns {Promise<boolean>}
*/
async function has_version_changed() {
if (import.meta.env.DEV) return false;

const headers = new Headers();
headers.append('pragma', 'no-cache');
headers.append('cache-control', 'no-cache');

const current_version = import.meta.env.VITE_APP_VERSION;
const new_version = await fetch('_app/version.json', { headers })
pzerelles marked this conversation as resolved.
Show resolved Hide resolved
.then((res) => res.json())
.catch(() => undefined);

return new_version && current_version && new_version !== current_version;
}

/**
* @param {RequestInfo} resource
* @param {RequestInit} [opts]
Expand Down Expand Up @@ -108,7 +147,17 @@ export class Renderer {
url: notifiable_store({}),
page: notifiable_store({}),
navigating: writable(/** @type {Navigating | null} */ (null)),
session: writable(session)
session: writable(session),
updated: checkable_store(
false,
(set) => {
return has_version_changed().then((changed) => {
if (changed) set(true);
return changed;
});
},
300000
pzerelles marked this conversation as resolved.
Show resolved Hide resolved
)
};

this.$session = null;
Expand Down Expand Up @@ -228,7 +277,7 @@ export class Renderer {
* @param {import('./types').NavigationInfo} info
* @param {string[]} chain
* @param {boolean} no_cache
* @param {{hash?: string, scroll: { x: number, y: number } | null, keepfocus: boolean}} [opts]
* @param {{hash?: string, scroll: { x: number, y: number } | null, keepfocus: boolean, onError: RouteOnErrorValue}} [opts]
*/
async handle_navigation(info, chain, no_cache, opts) {
if (this.started) {
Expand All @@ -245,10 +294,11 @@ export class Renderer {
* @param {import('./types').NavigationInfo} info
* @param {string[]} chain
* @param {boolean} no_cache
* @param {{hash?: string, scroll: { x: number, y: number } | null, keepfocus: boolean}} [opts]
* @param {{hash?: string, scroll: { x: number, y: number } | null, keepfocus: boolean, onError: RouteOnErrorValue}} [opts]
*/
async update(info, chain, no_cache, opts) {
const token = (this.token = {});
const onError = opts?.onError || 'fail';
let navigation_result = await this._get_navigation_result(info, no_cache);

// abort if user navigated during update
Expand All @@ -274,6 +324,12 @@ export class Renderer {
location.href = new URL(navigation_result.redirect, location.href).href;
}

return;
}
} else if (navigation_result.props?.page?.status >= 400 && onError !== 'fail') {
const updated = await this.stores.updated.check();
if (updated) {
location.href = info.url.href;
return;
}
}
Expand Down
11 changes: 8 additions & 3 deletions packages/kit/src/runtime/client/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@ export class Router {
* base: string;
* routes: import('types/internal').CSRRoute[];
* trailing_slash: import('types/internal').TrailingSlash;
* renderer: import('./renderer').Renderer
* renderer: import('./renderer').Renderer;
* onError?: import('types/config').RouteOnErrorValue;
* }} opts
*/
constructor({ base, routes, trailing_slash, renderer }) {
constructor({ base, routes, trailing_slash, renderer, onError }) {
this.base = base;
this.routes = routes;
this.trailing_slash = trailing_slash;
Expand All @@ -49,6 +50,9 @@ export class Router {
this.renderer = renderer;
renderer.router = this;

/** @type {import('types/config').RouteOnErrorValue} */
this.onError = onError || 'fail';

this.enabled = true;

// make it possible to reset focus
Expand Down Expand Up @@ -407,7 +411,8 @@ export class Router {

await this.renderer.handle_navigation(info, chain, false, {
scroll,
keepfocus
keepfocus,
onError: this.onError
});

this.navigating--;
Expand Down
23 changes: 14 additions & 9 deletions packages/kit/src/runtime/client/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ import { set_paths } from '../paths.js';
* },
* target: Node;
* session: any;
* route: boolean;
* route: {
* enabled: boolean;
* onError?: import('types/config').RouteOnErrorValue;
* },
* spa: boolean;
* trailing_slash: import('types/internal').TrailingSlash;
* hydrate: {
Expand All @@ -38,14 +41,16 @@ export async function start({ paths, target, session, route, spa, trailing_slash
session
});

const router = route
? new Router({
base: paths.base,
routes,
trailing_slash,
renderer
})
: null;
const router =
route && route.enabled
? new Router({
base: paths.base,
routes,
trailing_slash,
renderer,
onError: route.onError
})
: null;

init({ router, renderer });
set_paths(paths);
Expand Down
18 changes: 16 additions & 2 deletions packages/kit/src/runtime/server/page/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,23 @@ export async function render_response({

const session = writable($session);

const updated = () => {
const { set, update, subscribe } = writable(false);
return {
set,
update,
subscribe,
check: () => {}
};
};

/** @type {Record<string, any>} */
const props = {
stores: {
page: writable(null),
navigating: writable(null),
session
session,
updated
},
page: {
url: state.prerender ? create_prerendering_url_proxy(url) : url,
Expand Down Expand Up @@ -155,7 +166,10 @@ export async function render_response({
session: ${try_serialize($session, (error) => {
throw new Error(`Failed to serialize session data: ${error.message}`);
})},
route: ${!!page_config.router},
route: {
enabled: ${!!page_config.router},
onError: ${s(options.router.onError)}
},
spa: ${!ssr},
trailing_slash: ${s(options.trailing_slash)},
hydrate: ${ssr && page_config.hydrate ? `{
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/runtime/server/page/respond.js
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ function get_page_config(leaf, options) {
}

return {
router: 'router' in leaf ? !!leaf.router : options.router,
router: 'router' in leaf ? !!leaf.router : options.router.enabled,
hydrate: 'hydrate' in leaf ? !!leaf.hydrate : options.hydrate
};
}
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/runtime/server/page/respond_with_error.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export async function respond_with_error({ event, options, state, $session, stat
$session,
page_config: {
hydrate: options.hydrate,
router: options.router
router: options.router.enabled
},
stuff: error_loaded.stuff,
status,
Expand Down
Loading