diff --git a/.changeset/eighty-turkeys-exercise.md b/.changeset/eighty-turkeys-exercise.md new file mode 100644 index 000000000000..7815fd251150 --- /dev/null +++ b/.changeset/eighty-turkeys-exercise.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +[fix] harmonize cookie path and add dev time warnings diff --git a/.changeset/lucky-moles-count.md b/.changeset/lucky-moles-count.md new file mode 100644 index 000000000000..b0ffa91d7961 --- /dev/null +++ b/.changeset/lucky-moles-count.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/kit": patch +--- + +Restore `req.url` to `req.originalUrl` in dev and preview diff --git a/.changeset/pre.json b/.changeset/pre.json index 17f56dd210f0..1771db8dd81d 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -274,6 +274,7 @@ "eighty-candles-grow", "eighty-carrots-heal", "eighty-parrots-cry", + "eighty-turkeys-exercise", "eighty-waves-obey", "eleven-buckets-deny", "eleven-bugs-fail", @@ -648,6 +649,7 @@ "lucky-bottles-kick", "lucky-glasses-sell", "lucky-guests-act", + "lucky-moles-count", "lucky-phones-march", "lucky-plums-listen", "many-cups-report", @@ -1292,6 +1294,7 @@ "tidy-rocks-beg", "tidy-turkeys-rule", "tidy-wasps-shave", + "tidy-wombats-repeat", "tiny-badgers-love", "tiny-candles-repeat", "tiny-eels-hear", diff --git a/.changeset/tidy-wombats-repeat.md b/.changeset/tidy-wombats-repeat.md new file mode 100644 index 000000000000..d236ecbbf6ed --- /dev/null +++ b/.changeset/tidy-wombats-repeat.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +[feat] add invalidateAll option to goto diff --git a/documentation/docs/20-core-concepts/20-load.md b/documentation/docs/20-core-concepts/20-load.md index ca04ed73b8ed..759f53499891 100644 --- a/documentation/docs/20-core-concepts/20-load.md +++ b/documentation/docs/20-core-concepts/20-load.md @@ -266,6 +266,8 @@ export async function load({ cookies }) { } ``` +> When setting cookies, be aware of the `path` property. By default, the `path` of a cookie is the current pathname. If you for example set a cookie at page `admin/user`, the cookie will only be available within the `admin` pages by default. In most cases you likely want to set `path` to `'/'` to make the cookie available throughout your app. + Both server-only and shared `load` functions have access to a `setHeaders` function that, when running on the server, can set headers for the response. (When running in the browser, `setHeaders` has no effect.) This is useful if you want the page to be cached, for example: ```js diff --git a/documentation/docs/30-advanced/70-packaging.md b/documentation/docs/30-advanced/70-packaging.md index 1cbaead0b476..2b61424f5081 100644 --- a/documentation/docs/30-advanced/70-packaging.md +++ b/documentation/docs/30-advanced/70-packaging.md @@ -8,7 +8,7 @@ You can use SvelteKit to build apps as well as component libraries, using the `@ When you're creating an app, the contents of `src/routes` is the public-facing stuff; [`src/lib`](/docs/modules#$lib) contains your app's internal library. -A SvelteKit component library has the exact same structure as a SvelteKit app, except that `src/lib` is the public-facing bit. `src/routes` might be a documentation or demo site that accompanies the library, or it might just be a sandbox you use during development. +A component library has the exact same structure as a SvelteKit app, except that `src/lib` is the public-facing bit. `src/routes` might be a documentation or demo site that accompanies the library, or it might just be a sandbox you use during development. Running the `svelte-package` command from `@sveltejs/package` will take the contents of `src/lib` and generate a `package` directory (which can be [configured](/docs/configuration#package)) containing the following: @@ -38,6 +38,8 @@ declare module 'your-library/Foo.svelte'; import Foo from 'your-library/Foo.svelte'; ``` +> You should avoid using [SvelteKit-specific modules](/docs/modules) like `$app` in your packages unless you intend for them to only be consumable by other SvelteKit projects. E.g. rather than using `import { browser } from '$app/environment'` you could use [`import.meta.env.SSR`](https://vitejs.dev/guide/env-and-mode.html#env-variables) to make the library available to all Vite-based projects or better yet use [Node conditional exports](https://nodejs.org/api/packages.html#conditional-exports) to make it work for all bundlers. You may also wish to pass in things like the current URL or a navigation action as a prop rather than relying directly on `$app/stores`, `$app/navigation`, etc. Writing your app in this more generic fashion will also make it easier to setup tools for testing, UI demos and so on. + ### Options `svelte-package` accepts the following options: diff --git a/packages/adapter-static/package.json b/packages/adapter-static/package.json index 7c06af57a101..ff4d7865ba7d 100644 --- a/packages/adapter-static/package.json +++ b/packages/adapter-static/package.json @@ -31,6 +31,6 @@ "svelte": "^3.52.0", "typescript": "^4.8.4", "uvu": "^0.5.6", - "vite": "^3.1.8" + "vite": "^3.2.1" } } diff --git a/packages/adapter-static/test/apps/prerendered/package.json b/packages/adapter-static/test/apps/prerendered/package.json index 4e46e63f0602..b36d5c70beb1 100644 --- a/packages/adapter-static/test/apps/prerendered/package.json +++ b/packages/adapter-static/test/apps/prerendered/package.json @@ -10,7 +10,7 @@ "devDependencies": { "@sveltejs/kit": "workspace:*", "svelte": "^3.52.0", - "vite": "^3.1.8" + "vite": "^3.2.1" }, "type": "module" } diff --git a/packages/adapter-static/test/apps/spa/package.json b/packages/adapter-static/test/apps/spa/package.json index 0cfafec80838..7c397d71db5f 100644 --- a/packages/adapter-static/test/apps/spa/package.json +++ b/packages/adapter-static/test/apps/spa/package.json @@ -12,7 +12,7 @@ "@sveltejs/kit": "workspace:*", "sirv-cli": "^2.0.2", "svelte": "^3.52.0", - "vite": "^3.1.8" + "vite": "^3.2.1" }, "type": "module" } diff --git a/packages/create-svelte/templates/default/package.json b/packages/create-svelte/templates/default/package.json index f42ca737d6cb..9f9d03e0d961 100644 --- a/packages/create-svelte/templates/default/package.json +++ b/packages/create-svelte/templates/default/package.json @@ -14,7 +14,7 @@ "svelte": "^3.52.0", "svelte-preprocess": "^4.10.7", "typescript": "^4.8.4", - "vite": "^3.1.8" + "vite": "^3.2.1" }, "type": "module", "dependencies": { diff --git a/packages/kit/CHANGELOG.md b/packages/kit/CHANGELOG.md index aa83567229bd..670815911500 100644 --- a/packages/kit/CHANGELOG.md +++ b/packages/kit/CHANGELOG.md @@ -1,5 +1,19 @@ # @sveltejs/kit +## 1.0.0-next.528 + +### Patch Changes + +- Restore `req.url` to `req.originalUrl` in dev and preview ([#7343](https://github.com/sveltejs/kit/pull/7343)) + +## 1.0.0-next.527 + +### Patch Changes + +- [fix] harmonize cookie path and add dev time warnings ([#7416](https://github.com/sveltejs/kit/pull/7416)) + +* [feat] add invalidateAll option to goto ([#7407](https://github.com/sveltejs/kit/pull/7407)) + ## 1.0.0-next.526 ### Patch Changes diff --git a/packages/kit/package.json b/packages/kit/package.json index 3b6da178409a..1b91a2ea9038 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -1,6 +1,6 @@ { "name": "@sveltejs/kit", - "version": "1.0.0-next.526", + "version": "1.0.0-next.528", "repository": { "type": "git", "url": "https://github.com/sveltejs/kit", @@ -37,7 +37,7 @@ "svelte-preprocess": "^4.10.7", "typescript": "^4.8.4", "uvu": "^0.5.6", - "vite": "^3.1.8" + "vite": "^3.2.1" }, "peerDependencies": { "svelte": "^3.44.0", diff --git a/packages/kit/src/constants.js b/packages/kit/src/constants.js index ca14db293495..8dc9aab27904 100644 --- a/packages/kit/src/constants.js +++ b/packages/kit/src/constants.js @@ -3,5 +3,3 @@ export const SVELTE_KIT_ASSETS = '/_svelte_kit_assets'; export const GENERATED_COMMENT = '// this file is generated — do not edit it\n'; - -export const DATA_SUFFIX = '/__data.json'; diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js index f8581d2cc614..4305bd73a775 100644 --- a/packages/kit/src/exports/vite/dev/index.js +++ b/packages/kit/src/exports/vite/dev/index.js @@ -291,6 +291,8 @@ export async function dev(vite, vite_config, svelte_config) { remove_static_middlewares(vite.middlewares); vite.middlewares.use(async (req, res) => { + // Vite's base middleware strips out the base path. Restore it + req.url = req.originalUrl; try { const base = `${vite.config.server.https ? 'https' : 'http'}://${ req.headers[':authority'] || req.headers.host diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 70287c9106a1..cc72a61b883f 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -1,5 +1,5 @@ import { onMount, tick } from 'svelte'; -import { make_trackable, decode_params, normalize_path } from '../../utils/url.js'; +import { make_trackable, decode_params, normalize_path, add_data_suffix } from '../../utils/url.js'; import { find_anchor, get_base_uri, scroll_state } from './utils.js'; import { lock_fetch, @@ -14,7 +14,6 @@ import Root from '__GENERATED__/root.svelte'; import { nodes, server_loads, dictionary, matchers, hooks } from '__GENERATED__/client-manifest.js'; import { HttpError, Redirect } from '../control.js'; import { stores } from './singletons.js'; -import { DATA_SUFFIX } from '../../constants.js'; import { unwrap_promises } from '../../utils/promises.js'; import * as devalue from 'devalue'; @@ -164,13 +163,19 @@ export function create_client({ target, base, trailing_slash }) { /** * @param {string | URL} url - * @param {{ noscroll?: boolean; replaceState?: boolean; keepfocus?: boolean; state?: any }} opts + * @param {{ noscroll?: boolean; replaceState?: boolean; keepfocus?: boolean; state?: any; invalidateAll?: boolean }} opts * @param {string[]} redirect_chain * @param {{}} [nav_token] */ async function goto( url, - { noscroll = false, replaceState = false, keepfocus = false, state = {} }, + { + noscroll = false, + replaceState = false, + keepfocus = false, + state = {}, + invalidateAll = false + }, redirect_chain, nav_token ) { @@ -188,7 +193,11 @@ export function create_client({ target, base, trailing_slash }) { replaceState }, nav_token, - accepted: () => {}, + accepted: () => { + if (invalidateAll) { + force_invalidation = true; + } + }, blocked: () => {}, type: 'goto' }); @@ -1212,7 +1221,7 @@ export function create_client({ target, base, trailing_slash }) { post_update(); } } else if (result.type === 'redirect') { - goto(result.location, {}, []); + goto(result.location, { invalidateAll: true }, []); } else { /** @type {Record} */ const props = { @@ -1501,7 +1510,7 @@ export function create_client({ target, base, trailing_slash }) { */ async function load_data(url, invalid) { const data_url = new URL(url); - data_url.pathname = url.pathname.replace(/\/$/, '') + DATA_SUFFIX; + data_url.pathname = add_data_suffix(url.pathname); const res = await native_fetch(data_url.href, { headers: { diff --git a/packages/kit/src/runtime/server/cookie.js b/packages/kit/src/runtime/server/cookie.js index 12148144dc81..a6a4a2f9f15f 100644 --- a/packages/kit/src/runtime/server/cookie.js +++ b/packages/kit/src/runtime/server/cookie.js @@ -1,10 +1,19 @@ import { parse, serialize } from 'cookie'; +import { has_data_suffix, normalize_path, strip_data_suffix } from '../../utils/url.js'; + +/** + * Tracks all cookies set during dev mode so we can emit warnings + * when we detect that there's likely cookie misusage due to wrong paths + * + * @type {Record>} */ +const cookie_paths = {}; /** * @param {Request} request * @param {URL} url + * @param {Pick} options */ -export function get_cookies(request, url) { +export function get_cookies(request, url, options) { const header = request.headers.get('cookie') ?? ''; const initial_cookies = parse(header); @@ -42,7 +51,17 @@ export function get_cookies(request, url) { const decode = opts?.decode || decodeURIComponent; const req_cookies = parse(header, { decode }); - return req_cookies[name]; // the decoded string or undefined + const cookie = req_cookies[name]; // the decoded string or undefined + + if (!options.dev || cookie) { + return cookie; + } + + if (c || cookie_paths[name]?.size > 0) { + console.warn( + `Cookie with name '${name}' was not found, but a cookie with that name exists at a sub path. Did you mean to set its 'path' to '/'?` + ); + } }, /** @@ -51,14 +70,43 @@ export function get_cookies(request, url) { * @param {import('cookie').CookieSerializeOptions} opts */ set(name, value, opts = {}) { + let path = opts.path; + if (!path) { + const normalized = normalize_path( + // Remove suffix: 'foo/__data.json' would mean the cookie path is '/foo', + // whereas a direct hit of /foo would mean the cookie path is '/' + has_data_suffix(url.pathname) ? strip_data_suffix(url.pathname) : url.pathname, + options.trailing_slash + ); + // Emulate browser-behavior: if the cookie is set at '/foo/bar', its path is '/foo' + path = normalized.split('/').slice(0, -1).join('/') || '/'; + } + new_cookies[name] = { name, value, options: { ...defaults, - ...opts + ...opts, + path } }; + + if (options.dev) { + cookie_paths[name] = cookie_paths[name] || new Set(); + if (!value) { + if (!cookie_paths[name].has(path) && cookie_paths[name].size > 0) { + console.warn( + `Trying to delete cookie '${name}' at path '${path}', but a cookie with that name only exists at a different path.` + ); + } + cookie_paths[name].delete(path); + } else { + // We could also emit a warning here if the cookie already exists at a different path, + // but that's more likely a false positive because it's valid to set the same name at different paths + cookie_paths[name].add(path); + } + } }, /** @@ -66,15 +114,10 @@ export function get_cookies(request, url) { * @param {import('cookie').CookieSerializeOptions} opts */ delete(name, opts = {}) { - new_cookies[name] = { - name, - value: '', - options: { - ...defaults, - ...opts, - maxAge: 0 - } - }; + cookies.set(name, '', { + ...opts, + maxAge: 0 + }); }, /** diff --git a/packages/kit/src/runtime/server/cookie.spec.js b/packages/kit/src/runtime/server/cookie.spec.js index 9696e9152c5c..34ca3d0e83f0 100644 --- a/packages/kit/src/runtime/server/cookie.spec.js +++ b/packages/kit/src/runtime/server/cookie.spec.js @@ -42,15 +42,15 @@ paths.negative.forEach(([path, constraint]) => { }); }); -/** @param {boolean} localhost */ -const cookies_setup = (localhost = false) => { - const url = new URL(localhost ? 'http://localhost:1234' : 'https://example.com'); +/** @param {string} href */ +const cookies_setup = (href = 'https://example.com') => { + const url = new URL(href); const request = new Request(url, { headers: new Headers({ cookie: 'a=b;' }) }); - return get_cookies(request, url); + return get_cookies(request, url, { dev: false, trailing_slash: 'ignore' }); }; test('a cookie should not be present after it is deleted', () => { @@ -67,12 +67,22 @@ test('default values when set is called', () => { const opts = new_cookies['a']?.options; assert.equal(opts?.secure, true); assert.equal(opts?.httpOnly, true); - assert.equal(opts?.path, undefined); + assert.equal(opts?.path, '/'); + assert.equal(opts?.sameSite, 'lax'); +}); + +test('default values when set is called on sub path', () => { + const { cookies, new_cookies } = cookies_setup('https://example.com/foo/bar'); + cookies.set('a', 'b'); + const opts = new_cookies['a']?.options; + assert.equal(opts?.secure, true); + assert.equal(opts?.httpOnly, true); + assert.equal(opts?.path, '/foo'); assert.equal(opts?.sameSite, 'lax'); }); test('default values when on localhost', () => { - const { cookies, new_cookies } = cookies_setup(true); + const { cookies, new_cookies } = cookies_setup('http://localhost:1234'); cookies.set('a', 'b'); const opts = new_cookies['a']?.options; assert.equal(opts?.secure, false); @@ -94,7 +104,7 @@ test('default values when delete is called', () => { const opts = new_cookies['a']?.options; assert.equal(opts?.secure, true); assert.equal(opts?.httpOnly, true); - assert.equal(opts?.path, undefined); + assert.equal(opts?.path, '/'); assert.equal(opts?.sameSite, 'lax'); assert.equal(opts?.maxAge, 0); }); diff --git a/packages/kit/src/runtime/server/data/index.js b/packages/kit/src/runtime/server/data/index.js index 33c9a68a71c0..79c54ce8b10b 100644 --- a/packages/kit/src/runtime/server/data/index.js +++ b/packages/kit/src/runtime/server/data/index.js @@ -3,8 +3,7 @@ import { normalize_error } from '../../../utils/error.js'; import { once } from '../../../utils/functions.js'; import { load_server_data } from '../page/load_data.js'; import { data_response, handle_error_and_jsonify } from '../utils.js'; -import { normalize_path } from '../../../utils/url.js'; -import { DATA_SUFFIX } from '../../../constants.js'; +import { normalize_path, strip_data_suffix } from '../../../utils/url.js'; /** * @param {import('types').RequestEvent} event @@ -31,10 +30,7 @@ export async function render_data(event, route, options, state) { let aborted = false; const url = new URL(event.url); - url.pathname = normalize_path( - url.pathname.slice(0, -DATA_SUFFIX.length), - options.trailing_slash - ); + url.pathname = normalize_path(strip_data_suffix(url.pathname), options.trailing_slash); const new_event = { ...event, url }; diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index b3936a34b90c..007e029d6b48 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -5,10 +5,15 @@ import { respond_with_error } from './page/respond_with_error.js'; import { coalesce_to_error } from '../../utils/error.js'; import { is_form_content_type } from '../../utils/http.js'; import { GENERIC_ERROR, handle_fatal_error } from './utils.js'; -import { decode_params, disable_search, normalize_path } from '../../utils/url.js'; +import { + decode_params, + disable_search, + has_data_suffix, + normalize_path, + strip_data_suffix +} from '../../utils/url.js'; import { exec } from '../../utils/routing.js'; import { render_data } from './data/index.js'; -import { DATA_SUFFIX } from '../../constants.js'; import { add_cookies_to_headers, get_cookies } from './cookie.js'; import { HttpError } from '../control.js'; import { create_fetch } from './fetch.js'; @@ -57,8 +62,8 @@ export async function respond(request, options, state) { decoded = decoded.slice(options.paths.base.length) || '/'; } - const is_data_request = decoded.endsWith(DATA_SUFFIX); - if (is_data_request) decoded = decoded.slice(0, -DATA_SUFFIX.length) || '/'; + const is_data_request = has_data_suffix(decoded); + if (is_data_request) decoded = strip_data_suffix(decoded); if (!state.prerendering?.fallback) { const matchers = await options.manifest._.matchers(); @@ -96,7 +101,7 @@ export async function respond(request, options, state) { /** @type {Record} */ const headers = {}; - const { cookies, new_cookies, get_cookie_header } = get_cookies(request, url); + const { cookies, new_cookies, get_cookie_header } = get_cookies(request, url, options); if (state.prerendering) disable_search(url); diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index e8bef275cde1..61a3118442a6 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -1,7 +1,7 @@ import * as devalue from 'devalue'; -import { DATA_SUFFIX } from '../../../constants.js'; import { compact } from '../../../utils/array.js'; import { normalize_error } from '../../../utils/error.js'; +import { add_data_suffix } from '../../../utils/url.js'; import { HttpError, Redirect } from '../../control.js'; import { get_option, @@ -76,7 +76,7 @@ export async function render_page(event, route, page, options, state, resolve_op } const should_prerender_data = nodes.some((node) => node?.server); - const data_pathname = event.url.pathname.replace(/\/$/, '') + DATA_SUFFIX; + const data_pathname = add_data_suffix(event.url.pathname); // it's crucial that we do this before returning the non-SSR response, otherwise // SvelteKit will erroneously believe that the path has been prerendered, diff --git a/packages/kit/src/runtime/server/utils.js b/packages/kit/src/runtime/server/utils.js index e2559e1a2e45..0f51a492a500 100644 --- a/packages/kit/src/runtime/server/utils.js +++ b/packages/kit/src/runtime/server/utils.js @@ -1,6 +1,6 @@ import * as devalue from 'devalue'; -import { DATA_SUFFIX } from '../../constants.js'; import { negotiate } from '../../utils/http.js'; +import { has_data_suffix } from '../../utils/url.js'; import { HttpError } from '../control.js'; /** @param {any} body */ @@ -144,7 +144,7 @@ export function handle_fatal_error(event, options, error) { 'text/html' ]); - if (event.url.pathname.endsWith(DATA_SUFFIX) || type === 'application/json') { + if (has_data_suffix(event.url.pathname) || type === 'application/json') { return new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json; charset=utf-8' } diff --git a/packages/kit/src/utils/url.js b/packages/kit/src/utils/url.js index 53d3b671dba6..1153e4d31d78 100644 --- a/packages/kit/src/utils/url.js +++ b/packages/kit/src/utils/url.js @@ -142,3 +142,20 @@ export function disable_search(url) { }); } } + +const DATA_SUFFIX = '/__data.json'; + +/** @param {string} pathname */ +export function has_data_suffix(pathname) { + return pathname.endsWith(DATA_SUFFIX); +} + +/** @param {string} pathname */ +export function add_data_suffix(pathname) { + return pathname.replace(/\/$/, '') + DATA_SUFFIX; +} + +/** @param {string} pathname */ +export function strip_data_suffix(pathname) { + return pathname.slice(0, -DATA_SUFFIX.length); +} diff --git a/packages/kit/test/apps/amp/package.json b/packages/kit/test/apps/amp/package.json index d611e45e19c0..449dc8cee65f 100644 --- a/packages/kit/test/apps/amp/package.json +++ b/packages/kit/test/apps/amp/package.json @@ -19,7 +19,7 @@ "svelte": "^3.52.0", "svelte-check": "^2.9.2", "typescript": "^4.8.4", - "vite": "^3.1.8" + "vite": "^3.2.1" }, "type": "module" } diff --git a/packages/kit/test/apps/basics/package.json b/packages/kit/test/apps/basics/package.json index 63fe7b7293f5..4fd661f28220 100644 --- a/packages/kit/test/apps/basics/package.json +++ b/packages/kit/test/apps/basics/package.json @@ -18,7 +18,7 @@ "svelte": "^3.52.0", "svelte-check": "^2.9.2", "typescript": "^4.8.4", - "vite": "^3.1.8" + "vite": "^3.2.1" }, "type": "module" } diff --git a/packages/kit/test/apps/basics/src/routes/load/invalidation/forced/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/invalidation/forced/+page.svelte index b66f248deba8..f7ebb7f7a132 100644 --- a/packages/kit/test/apps/basics/src/routes/load/invalidation/forced/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/load/invalidation/forced/+page.svelte @@ -1,5 +1,5 @@