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 22 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
11 changes: 11 additions & 0 deletions documentation/docs/14-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ const config = {
},
target: null,
trailingSlash: 'never',
version: {
name: Date.now(),
pzerelles marked this conversation as resolved.
Show resolved Hide resolved
pollInterval: 60_000,
pzerelles marked this conversation as resolved.
Show resolved Hide resolved
},
vite: () => ({})
},

Expand Down Expand Up @@ -253,6 +257,13 @@ Whether to remove, append, or ignore trailing slashes when resolving URLs to rou

> Ignoring trailing slashes is not recommended — the semantics of relative paths differ between the two cases (`./y` from `/x` is `/y`, but from `/x/` is `/x/y`), and `/x` and `/x/` are treated as separate URLs which is harmful to SEO. If you use this option, ensure that you implement logic for conditionally adding or removing trailing slashes from `request.path` inside your [`handle`](#hooks-handle) function.

### version

An object containing zero or more of the following values:

- `name` - current app version string
- `pollInterval` - interval in milliseconds to poll for version changes

Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved
### vite

A [Vite config object](https://vitejs.dev/config), or a function that returns one. You can pass [Vite and Rollup plugins](https://github.com/vitejs/awesome-vite#plugins) via [the `plugins` option](https://vitejs.dev/config/#plugins) to customize your build in advanced ways such as supporting image optimization, Tauri, WASM, Workbox, and more. SvelteKit will prevent you from setting certain build-related options since it depends on certain configuration values.
9 changes: 9 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,10 @@ export async function build_client({
output_dir,
client_entry_file
}) {
process.env.VITE_APP_VERSION = config.kit.version.name;
process.env.VITE_APP_VERSION_FILE = `${config.kit.appDir}/version.json`;
process.env.VITE_APP_VERSION_POLL_INTERVAL = `${config.kit.version.pollInterval}`;

create_app({
manifest_data,
output: `${SVELTE_KIT}/generated`,
Expand Down Expand Up @@ -105,6 +109,11 @@ 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({ version: process.env.VITE_APP_VERSION })
);

return {
assets,
chunks,
Expand Down
20 changes: 17 additions & 3 deletions packages/kit/src/core/config/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,11 @@ const get_defaults = (prefix = '') => ({
router: undefined,
ssr: undefined,
target: null,
trailingSlash: 'never'
trailingSlash: 'never',
version: {
name: Date.now().toString(),
pollInterval: 60_000
}
}
});

Expand All @@ -110,7 +114,10 @@ test('fills in defaults', () => {

remove_keys(validated, ([, v]) => typeof v === 'function');

assert.equal(validated, get_defaults());
const defaults = get_defaults();
defaults.kit.version.name = validated.kit.version.name;

assert.equal(validated, defaults);
});

test('errors on invalid values', () => {
Expand Down Expand Up @@ -159,6 +166,9 @@ test('fills in partial blanks', () => {
kit: {
files: {
assets: 'public'
},
version: {
name: '0'
}
}
});
Expand All @@ -172,6 +182,7 @@ test('fills in partial blanks', () => {

const config = get_defaults();
config.kit.files.assets = 'public';
config.kit.version.name = '0';

assert.equal(validated, config);
});
Expand Down Expand Up @@ -334,7 +345,10 @@ test('load default config (esm)', async () => {
const config = await load_config({ cwd });
remove_keys(config, ([, v]) => typeof v === 'function');

assert.equal(config, get_defaults(cwd + '/'));
const defaults = get_defaults(cwd + '/');
defaults.kit.version.name = config.kit.version.name;

assert.equal(config, defaults);
});

test('errors on loading config with incorrect default export', async () => {
Expand Down
5 changes: 5 additions & 0 deletions packages/kit/src/core/config/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,11 @@ const options = object(

trailingSlash: list(['never', 'always', 'ignore']),

version: object({
name: string(Date.now().toString()),
pollInterval: number(60_000)
}),

vite: validate(
() => ({}),
(input, keypath) => {
Expand Down
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')
};
63 changes: 62 additions & 1 deletion packages/kit/src/runtime/client/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { writable } from 'svelte/store';
import { coalesce_to_error } from '../../utils/error.js';
import { hash } from '../hash.js';
import { normalize } from '../load.js';
import { base } from '../paths.js';

/**
* @typedef {import('types/internal').CSRComponent} CSRComponent
Expand Down Expand Up @@ -39,6 +40,48 @@ 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 || import.meta.env.SSR) return false;

const file = import.meta.env.VITE_APP_VERSION_FILE;
const current_version = import.meta.env.VITE_APP_VERSION;
if (!file || !current_version) return false;

const new_version = await fetch(`${base}/${file}`, {
headers: {
pragma: 'no-cache',
'cache-control': 'no-cache'
}
})
.then((res) => res.json())
.then((res) => res.version);

return new_version && new_version !== current_version;
}

/**
* @param {RequestInfo} resource
* @param {RequestInit} [opts]
Expand Down Expand Up @@ -104,11 +147,23 @@ export class Renderer {
promise: null
};

const version_poll_interval = +import.meta.env.VITE_APP_VERSION_POLL_INTERVAL;

this.stores = {
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;
});
},
version_poll_interval
)
};

this.$session = null;
Expand Down Expand Up @@ -274,6 +329,12 @@ export class Renderer {
location.href = new URL(navigation_result.redirect, location.href).href;
}

return;
}
} else if (navigation_result.props?.page?.status >= 400) {
const updated = await this.stores.updated.check();
if (updated) {
location.href = info.url.href;
return;
}
}
Expand Down
13 changes: 12 additions & 1 deletion 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: () => {}
};
})();
pzerelles marked this conversation as resolved.
Show resolved Hide resolved

/** @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
6 changes: 6 additions & 0 deletions packages/kit/types/ambient-modules.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ declare module '$app/stores' {
navigating: typeof navigating;
page: typeof page;
session: Writable<Session>;
updated: Writable<boolean> & { check: () => boolean };
pzerelles marked this conversation as resolved.
Show resolved Hide resolved
};
/**
* A readable store whose value contains page data.
Expand All @@ -132,6 +133,11 @@ declare module '$app/stores' {
* It can be written to, but this will not cause changes to persist on the server — this is something you must implement yourself.
*/
export const session: Writable<any>;
/**
* A writable store indicating if the site was updated since the store was created.
* It can be written to when custom logic is required to detect updates.
*/
export const updated: Writable<boolean> & { check: () => boolean };
pzerelles marked this conversation as resolved.
Show resolved Hide resolved
}

declare module '$service-worker' {
Expand Down
4 changes: 4 additions & 0 deletions packages/kit/types/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@ export interface Config {
};
target?: string;
trailingSlash?: TrailingSlash;
version?: {
name?: string;
pollInterval?: number;
};
vite?: ViteConfig | (() => ViteConfig);
};
preprocess?: any;
Expand Down