From 2c0f23db69d17f36c2eb654bb87e31c33286640c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 18 Nov 2022 15:45:47 -0500 Subject: [PATCH 1/9] server-side trailing slash logic --- packages/kit/src/core/config/index.spec.js | 2 +- packages/kit/src/core/config/options.js | 11 +- .../src/exports/vite/build/build_server.js | 1 - packages/kit/src/exports/vite/dev/index.js | 1 - packages/kit/src/runtime/client/client.js | 3 +- packages/kit/src/runtime/client/start.js | 6 +- packages/kit/src/runtime/server/cookie.js | 13 +- .../kit/src/runtime/server/cookie.spec.js | 2 +- packages/kit/src/runtime/server/data/index.js | 5 +- packages/kit/src/runtime/server/index.js | 233 ++++++++++-------- .../kit/src/runtime/server/page/render.js | 1 - packages/kit/src/runtime/server/utils.js | 4 +- packages/kit/test/apps/basics/test/test.js | 2 +- .../options/source/pages/+layout.server.js | 1 + .../kit/test/apps/options/svelte.config.js | 1 - .../options/src/routes/+layout.js | 1 + .../trailing-slash/src/routes/+layout.js | 1 + packages/kit/types/internal.d.ts | 4 +- 18 files changed, 161 insertions(+), 131 deletions(-) create mode 100644 packages/kit/test/apps/options/source/pages/+layout.server.js diff --git a/packages/kit/src/core/config/index.spec.js b/packages/kit/src/core/config/index.spec.js index d4c87b4d5d26..90866aeec8d7 100644 --- a/packages/kit/src/core/config/index.spec.js +++ b/packages/kit/src/core/config/index.spec.js @@ -127,7 +127,7 @@ const get_defaults = (prefix = '') => ({ routes: undefined, ssr: undefined, target: undefined, - trailingSlash: 'never', + trailingSlash: undefined, version: { name: Date.now().toString(), pollInterval: 0 diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js index 8e1228e071e3..67b84152494a 100644 --- a/packages/kit/src/core/config/options.js +++ b/packages/kit/src/core/config/options.js @@ -343,7 +343,10 @@ const options = object( // TODO remove this for 1.0 target: error((keypath) => `${keypath} is no longer required, and should be removed`), - trailingSlash: list(['never', 'always', 'ignore']), + trailingSlash: error( + (keypath, input) => + `${keypath} has been removed. You can set \`export const trailingSlash = '${input}'\` inside the top level +layout.js instead. See the PR for more information: https://github.com/sveltejs/kit/pull/TODO` + ), version: object({ name: string(Date.now().toString()), @@ -506,10 +509,10 @@ function assert_string(input, keypath) { } } -/** @param {(keypath?: string) => string} fn */ +/** @param {(keypath?: string, input?: any) => string} fn */ function error(fn) { - return validate(undefined, (_, keypath) => { - throw new Error(fn(keypath)); + return validate(undefined, (input, keypath) => { + throw new Error(fn(keypath, input)); }); } diff --git a/packages/kit/src/exports/vite/build/build_server.js b/packages/kit/src/exports/vite/build/build_server.js index 36c655f88b86..866642f07d15 100644 --- a/packages/kit/src/exports/vite/build/build_server.js +++ b/packages/kit/src/exports/vite/build/build_server.js @@ -87,7 +87,6 @@ export class Server { app_template, app_template_contains_nonce: ${template.includes('%sveltekit.nonce%')}, error_template, - trailing_slash: ${s(config.kit.trailingSlash)}, version: ${s(config.kit.version.name)} }; } diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js index ae5591062fe4..6684163a5df5 100644 --- a/packages/kit/src/exports/vite/dev/index.js +++ b/packages/kit/src/exports/vite/dev/index.js @@ -480,7 +480,6 @@ export async function dev(vite, vite_config, svelte_config) { service_worker: svelte_config.kit.serviceWorker.register && !!resolve_entry(svelte_config.kit.files.serviceWorker), - trailing_slash: svelte_config.kit.trailingSlash, version: svelte_config.kit.version.name }, { diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index bf2d9f9940da..457107dbf50f 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -77,11 +77,10 @@ function check_for_removed_attributes() { * @param {{ * target: Element; * base: string; - * trailing_slash: import('types').TrailingSlash; * }} opts * @returns {import('./types').Client} */ -export function create_client({ target, base, trailing_slash }) { +export function create_client({ target, base }) { /** @type {Array<((url: URL) => boolean)>} */ const invalidated = []; diff --git a/packages/kit/src/runtime/client/start.js b/packages/kit/src/runtime/client/start.js index 00b0fd9a986d..941e564a6278 100644 --- a/packages/kit/src/runtime/client/start.js +++ b/packages/kit/src/runtime/client/start.js @@ -13,11 +13,10 @@ import { set_version } from '../env.js'; * base: string; * }, * target: Element; - * trailing_slash: import('types').TrailingSlash; * version: string; * }} opts */ -export async function start({ env, hydrate, paths, target, trailing_slash, version }) { +export async function start({ env, hydrate, paths, target, version }) { set_public_env(env); set_paths(paths); set_version(version); @@ -30,8 +29,7 @@ export async function start({ env, hydrate, paths, target, trailing_slash, versi const client = create_client({ target, - base: paths.base, - trailing_slash + base: paths.base }); init({ client }); diff --git a/packages/kit/src/runtime/server/cookie.js b/packages/kit/src/runtime/server/cookie.js index 75672fe57686..4496d87b7dbd 100644 --- a/packages/kit/src/runtime/server/cookie.js +++ b/packages/kit/src/runtime/server/cookie.js @@ -11,9 +11,10 @@ const cookie_paths = {}; /** * @param {Request} request * @param {URL} url - * @param {Pick} options + * @param {boolean} dev + * @param {import('types').TrailingSlash} trailing_slash */ -export function get_cookies(request, url, options) { +export function get_cookies(request, url, dev, trailing_slash) { const header = request.headers.get('cookie') ?? ''; const initial_cookies = parse(header); @@ -21,12 +22,12 @@ export function get_cookies(request, url, options) { // 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 + trailing_slash ); // Emulate browser-behavior: if the cookie is set at '/foo/bar', its path is '/foo' const default_path = normalized_url.split('/').slice(0, -1).join('/') || '/'; - if (options.dev) { + if (dev) { // Remove all cookies that no longer exist according to the request for (const name of Object.keys(cookie_paths)) { cookie_paths[name] = new Set( @@ -79,7 +80,7 @@ export function get_cookies(request, url, options) { const req_cookies = parse(header, { decode }); const cookie = req_cookies[name]; // the decoded string or undefined - if (!options.dev || cookie) { + if (!dev || cookie) { return cookie; } @@ -113,7 +114,7 @@ export function get_cookies(request, url, options) { } }; - if (options.dev) { + if (dev) { cookie_paths[name] = cookie_paths[name] ?? new Set(); if (!value) { if (!cookie_paths[name].has(path) && cookie_paths[name].size > 0) { diff --git a/packages/kit/src/runtime/server/cookie.spec.js b/packages/kit/src/runtime/server/cookie.spec.js index 34ca3d0e83f0..84f8bdc363e4 100644 --- a/packages/kit/src/runtime/server/cookie.spec.js +++ b/packages/kit/src/runtime/server/cookie.spec.js @@ -50,7 +50,7 @@ const cookies_setup = (href = 'https://example.com') => { cookie: 'a=b;' }) }); - return get_cookies(request, url, { dev: false, trailing_slash: 'ignore' }); + return get_cookies(request, url, false, 'ignore'); }; test('a cookie should not be present after it is deleted', () => { diff --git a/packages/kit/src/runtime/server/data/index.js b/packages/kit/src/runtime/server/data/index.js index ebd7a83758c7..44a0fe2967f6 100644 --- a/packages/kit/src/runtime/server/data/index.js +++ b/packages/kit/src/runtime/server/data/index.js @@ -12,9 +12,10 @@ export const INVALIDATED_HEADER = 'x-sveltekit-invalidated'; * @param {import('types').SSRRoute} route * @param {import('types').SSROptions} options * @param {import('types').SSRState} state + * @param {import('types').TrailingSlash} trailing_slash * @returns {Promise} */ -export async function render_data(event, route, options, state) { +export async function render_data(event, route, options, state, trailing_slash) { if (!route.page) { // requesting /__data.json should fail for a +server.js return new Response(undefined, { @@ -32,7 +33,7 @@ export async function render_data(event, route, options, state) { let aborted = false; const url = new URL(event.url); - url.pathname = normalize_path(strip_data_suffix(url.pathname), options.trailing_slash); + url.pathname = normalize_path(strip_data_suffix(url.pathname), 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 0d13c055c66b..a79589b9215d 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -3,7 +3,7 @@ import { render_page } from './page/index.js'; import { render_response } from './page/render.js'; import { respond_with_error } from './page/respond_with_error.js'; import { is_form_content_type } from '../../utils/http.js'; -import { GENERIC_ERROR, handle_fatal_error, redirect_response } from './utils.js'; +import { GENERIC_ERROR, get_option, handle_fatal_error, redirect_response } from './utils.js'; import { decode_pathname, decode_params, @@ -70,6 +70,7 @@ export async function respond(request, options, state) { if (is_data_request) decoded = strip_data_suffix(decoded) || '/'; if (!state.prerendering?.fallback) { + // TODO this could theoretically break — should probably be inside a try-catch const matchers = await options.manifest._.matchers(); for (const candidate of options.manifest._.routes) { @@ -85,34 +86,19 @@ export async function respond(request, options, state) { } } - if (route?.page && !is_data_request) { - const normalized = normalize_path(url.pathname, options.trailing_slash); - - if (normalized !== url.pathname && !state.prerendering?.fallback) { - return new Response(undefined, { - status: 301, - headers: { - 'x-sveltekit-normalize': '1', - location: - // ensure paths starting with '//' are not treated as protocol-relative - (normalized.startsWith('//') ? url.origin + normalized : normalized) + - (url.search === '?' ? '' : url.search) - } - }); - } - } + /** @type {import('types').TrailingSlash | void} */ + let trailing_slash = undefined; /** @type {Record} */ const headers = {}; - const { cookies, new_cookies, get_cookie_header } = get_cookies(request, url, options); - if (state.prerendering && !state.prerendering.fallback) disable_search(url); /** @type {import('types').RequestEvent} */ const event = { - cookies, - // @ts-expect-error this is added in the next step, because `create_fetch` needs a reference to `event` + // @ts-expect-error `cookies` and `fetch` need to be created after the `event` itself + cookies: null, + // @ts-expect-error fetch: null, getClientAddress: state.getClientAddress || @@ -149,8 +135,6 @@ export async function respond(request, options, state) { url }; - event.fetch = create_fetch({ event, options, state, get_cookie_header }); - // TODO remove this for 1.0 /** * @param {string} property @@ -193,6 +177,128 @@ export async function respond(request, options, state) { preload: default_preload }; + try { + // determine whether we need to redirect to add/remove a trailing slash + if (route && !is_data_request) { + if (route.page) { + const nodes = await Promise.all([ + // we use == here rather than === because [undefined] serializes as "[null]" + ...route.page.layouts.map((n) => (n == undefined ? n : options.manifest._.nodes[n]())), + options.manifest._.nodes[route.page.leaf]() + ]); + + console.log('page nodes', nodes); + + trailing_slash = get_option(nodes, 'trailingSlash'); + } else if (route.endpoint) { + const node = await route.endpoint(); + trailing_slash = node.trailingSlash; + } + + const normalized = normalize_path(url.pathname, trailing_slash ?? 'never'); + + if (normalized !== url.pathname && !state.prerendering?.fallback) { + return new Response(undefined, { + status: 301, + headers: { + 'x-sveltekit-normalize': '1', + location: + // ensure paths starting with '//' are not treated as protocol-relative + (normalized.startsWith('//') ? url.origin + normalized : normalized) + + (url.search === '?' ? '' : url.search) + } + }); + } + } + + const { cookies, new_cookies, get_cookie_header } = get_cookies( + request, + url, + options.dev, + trailing_slash ?? 'never' + ); + + event.cookies = cookies; + event.fetch = create_fetch({ event, options, state, get_cookie_header }); + + const response = await options.hooks.handle({ + event, + resolve: (event, opts) => + resolve(event, opts).then((response) => { + // add headers/cookies here, rather than inside `resolve`, so that we + // can do it once for all responses instead of once per `return` + for (const key in headers) { + const value = headers[key]; + response.headers.set(key, /** @type {string} */ (value)); + } + + if (is_data_request) { + // set the Vary header on __data.json requests to ensure we don't cache + // incomplete responses with skipped data loads + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary + const vary = response.headers.get('Vary'); + if (vary !== '*') { + response.headers.append('Vary', INVALIDATED_HEADER); + } + } + + add_cookies_to_headers(response.headers, Object.values(new_cookies)); + + if (state.prerendering && event.route.id !== null) { + response.headers.set('x-sveltekit-routeid', encodeURI(event.route.id)); + } + + return response; + }), + // TODO remove for 1.0 + // @ts-expect-error + get request() { + throw new Error('request in handle has been replaced with event' + details); + } + }); + + // respond with 304 if etag matches + if (response.status === 200 && response.headers.has('etag')) { + let if_none_match_value = request.headers.get('if-none-match'); + + // ignore W/ prefix https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match#directives + if (if_none_match_value?.startsWith('W/"')) { + if_none_match_value = if_none_match_value.substring(2); + } + + const etag = /** @type {string} */ (response.headers.get('etag')); + + if (if_none_match_value === etag) { + const headers = new Headers({ etag }); + + // https://datatracker.ietf.org/doc/html/rfc7232#section-4.1 + set-cookie + for (const key of [ + 'cache-control', + 'content-location', + 'date', + 'expires', + 'vary', + 'set-cookie' + ]) { + const value = response.headers.get(key); + if (value) headers.set(key, value); + } + + return new Response(undefined, { + status: 304, + headers + }); + } + } + + return response; + } catch (error) { + if (error instanceof Redirect) { + return redirect_response(error.status, error.location); + } + return handle_fatal_error(event, options, error); + } + /** * * @param {import('types').RequestEvent} event @@ -240,7 +346,7 @@ export async function respond(request, options, state) { let response; if (is_data_request) { - response = await render_data(event, route, options, state); + response = await render_data(event, route, options, state, trailing_slash ?? 'never'); } else if (route.endpoint && (!route.page || is_endpoint_request(event))) { response = await render_endpoint(event, await route.endpoint(), state); } else if (route.page) { @@ -293,83 +399,4 @@ export async function respond(request, options, state) { }; } } - - try { - const response = await options.hooks.handle({ - event, - resolve: (event, opts) => - resolve(event, opts).then((response) => { - // add headers/cookies here, rather than inside `resolve`, so that we - // can do it once for all responses instead of once per `return` - for (const key in headers) { - const value = headers[key]; - response.headers.set(key, /** @type {string} */ (value)); - } - - if (is_data_request) { - // set the Vary header on __data.json requests to ensure we don't cache - // incomplete responses with skipped data loads - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary - const vary = response.headers.get('Vary'); - if (vary !== '*') { - response.headers.append('Vary', INVALIDATED_HEADER); - } - } - - add_cookies_to_headers(response.headers, Object.values(new_cookies)); - - if (state.prerendering && event.route.id !== null) { - response.headers.set('x-sveltekit-routeid', encodeURI(event.route.id)); - } - - return response; - }), - // TODO remove for 1.0 - // @ts-expect-error - get request() { - throw new Error('request in handle has been replaced with event' + details); - } - }); - - // respond with 304 if etag matches - if (response.status === 200 && response.headers.has('etag')) { - let if_none_match_value = request.headers.get('if-none-match'); - - // ignore W/ prefix https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match#directives - if (if_none_match_value?.startsWith('W/"')) { - if_none_match_value = if_none_match_value.substring(2); - } - - const etag = /** @type {string} */ (response.headers.get('etag')); - - if (if_none_match_value === etag) { - const headers = new Headers({ etag }); - - // https://datatracker.ietf.org/doc/html/rfc7232#section-4.1 + set-cookie - for (const key of [ - 'cache-control', - 'content-location', - 'date', - 'expires', - 'vary', - 'set-cookie' - ]) { - const value = response.headers.get(key); - if (value) headers.set(key, value); - } - - return new Response(undefined, { - status: 304, - headers - }); - } - } - - return response; - } catch (error) { - if (error instanceof Redirect) { - return redirect_response(error.status, error.location); - } - return handle_fatal_error(event, options, error); - } } diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index bcde19fdeece..e386a8293153 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -281,7 +281,6 @@ export async function render_response({ }` : 'null'}, paths: ${s(options.paths)}, target: document.querySelector('[data-sveltekit-hydrate="${target}"]').parentNode, - trailing_slash: ${s(options.trailing_slash)}, version: ${s(options.version)} }); `; diff --git a/packages/kit/src/runtime/server/utils.js b/packages/kit/src/runtime/server/utils.js index 488300d144b4..3fe10b903b26 100644 --- a/packages/kit/src/runtime/server/utils.js +++ b/packages/kit/src/runtime/server/utils.js @@ -69,8 +69,8 @@ export function allowed_methods(mod) { } /** - * @template {'prerender' | 'ssr' | 'csr'} Option - * @template {Option extends 'prerender' ? import('types').PrerenderOption : boolean} Value + * @template {'prerender' | 'ssr' | 'csr' | 'trailingSlash'} Option + * @template {Option extends 'prerender' ? import('types').PrerenderOption : Option extends 'trailingSlash' ? import('types').TrailingSlash : boolean} Value * * @param {Array} nodes * @param {Option} option diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index 2ee921709940..2fbac254cd8a 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -1331,7 +1331,7 @@ test.describe('Redirects', () => { }); test.describe('Routing', () => { - test('redirects from /routing/ to /routing', async ({ + test.only('redirects from /routing/ to /routing', async ({ baseURL, page, clicknav, diff --git a/packages/kit/test/apps/options/source/pages/+layout.server.js b/packages/kit/test/apps/options/source/pages/+layout.server.js new file mode 100644 index 000000000000..d3c325085ed2 --- /dev/null +++ b/packages/kit/test/apps/options/source/pages/+layout.server.js @@ -0,0 +1 @@ +export const trailingSlash = 'always'; diff --git a/packages/kit/test/apps/options/svelte.config.js b/packages/kit/test/apps/options/svelte.config.js index 2246d8b5b928..bf17940ba1b4 100644 --- a/packages/kit/test/apps/options/svelte.config.js +++ b/packages/kit/test/apps/options/svelte.config.js @@ -22,7 +22,6 @@ const config = { appDir: '_wheee', inlineStyleThreshold: 1024, outDir: '.custom-out-dir', - trailingSlash: 'always', paths: { base: '/path-base', assets: 'https://cdn.example.com/stuff' diff --git a/packages/kit/test/prerendering/options/src/routes/+layout.js b/packages/kit/test/prerendering/options/src/routes/+layout.js index 189f71e2e1b3..ba58d86071e4 100644 --- a/packages/kit/test/prerendering/options/src/routes/+layout.js +++ b/packages/kit/test/prerendering/options/src/routes/+layout.js @@ -1 +1,2 @@ export const prerender = true; +export const trailingSlash = 'always'; diff --git a/packages/kit/test/prerendering/trailing-slash/src/routes/+layout.js b/packages/kit/test/prerendering/trailing-slash/src/routes/+layout.js index 189f71e2e1b3..ba58d86071e4 100644 --- a/packages/kit/test/prerendering/trailing-slash/src/routes/+layout.js +++ b/packages/kit/test/prerendering/trailing-slash/src/routes/+layout.js @@ -1 +1,2 @@ export const prerender = true; +export const trailingSlash = 'always'; diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts index aa8dea77ba71..31ed01725e0c 100644 --- a/packages/kit/types/internal.d.ts +++ b/packages/kit/types/internal.d.ts @@ -266,6 +266,7 @@ export interface SSRNode { prerender?: PrerenderOption; ssr?: boolean; csr?: boolean; + trailingSlash?: TrailingSlash; }; server: { @@ -273,6 +274,7 @@ export interface SSRNode { prerender?: PrerenderOption; ssr?: boolean; csr?: boolean; + trailingSlash?: TrailingSlash; actions?: Actions; }; @@ -312,7 +314,6 @@ export interface SSROptions { }): string; app_template_contains_nonce: boolean; error_template({ message, status }: { message: string; status: number }): string; - trailing_slash: TrailingSlash; version: string; } @@ -328,6 +329,7 @@ export interface PageNodeIndexes { export type SSREndpoint = Partial> & { prerender?: PrerenderOption; + trailingSlash?: TrailingSlash; }; export interface SSRRoute { From 263a929ef0c1139adf32254b85f2676446847f10 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 18 Nov 2022 17:38:07 -0500 Subject: [PATCH 2/9] make it work for client-side navigation, and for endpoints --- packages/kit/src/core/config/options.js | 4 ++-- packages/kit/src/runtime/client/client.js | 21 ++++++++++++------- packages/kit/src/runtime/client/types.d.ts | 4 +++- packages/kit/src/runtime/server/index.js | 2 -- .../kit/src/runtime/server/page/load_data.js | 3 ++- .../kit/src/runtime/server/page/render.js | 4 +++- packages/kit/src/runtime/server/utils.js | 4 +++- packages/kit/test/apps/basics/test/test.js | 2 +- .../pages/endpoint-with-slash/+server.js | 5 +++++ packages/kit/test/apps/options/test/test.js | 19 ++++++++++++++--- .../trailing-slash/svelte.config.js | 4 +--- packages/kit/types/internal.d.ts | 2 ++ 12 files changed, 52 insertions(+), 22 deletions(-) create mode 100644 packages/kit/test/apps/options/source/pages/endpoint-with-slash/+server.js diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js index 67b84152494a..2288be6da306 100644 --- a/packages/kit/src/core/config/options.js +++ b/packages/kit/src/core/config/options.js @@ -320,7 +320,7 @@ const options = object( // TODO remove for 1.0 router: error( (keypath) => - `${keypath} has been removed. You can set \`export const csr = false\` inside the top level +layout.js instead. See the PR for more information: https://github.com/sveltejs/kit/pull/6197` + `${keypath} has been removed. You can set \`export const csr = false\` inside the top level +layout.js (or +layout.server.js) instead. See the PR for more information: https://github.com/sveltejs/kit/pull/6197` ), // TODO remove for 1.0 @@ -345,7 +345,7 @@ const options = object( trailingSlash: error( (keypath, input) => - `${keypath} has been removed. You can set \`export const trailingSlash = '${input}'\` inside the top level +layout.js instead. See the PR for more information: https://github.com/sveltejs/kit/pull/TODO` + `${keypath} has been removed. You can set \`export const trailingSlash = '${input}'\` inside a top level +layout.js (or +layout.server.js) instead. See the PR for more information: https://github.com/sveltejs/kit/pull/TODO` ), version: object({ diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 457107dbf50f..731288c829b6 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -415,6 +415,14 @@ export function create_client({ target, base }) { }) { const filtered = /** @type {import('./types').BranchNode[] } */ (branch.filter(Boolean)); + /** @type {import('types').TrailingSlash} */ + let slash = 'never'; + for (const node of branch) { + if (node?.slash !== undefined) slash = node.slash; + } + url.pathname = normalize_path(url.pathname, slash); + url.search = url.search; // turn `/?` into `/` + /** @type {import('./types').NavigationFinished} */ const result = { type: 'loaded', @@ -656,7 +664,8 @@ export function create_client({ target, base }) { loader, server: server_data_node, shared: node.shared?.load ? { type: 'data', data, uses } : null, - data: data ?? server_data_node?.data ?? null + data: data ?? server_data_node?.data ?? null, + slash: node.shared?.trailingSlash ?? server_data_node?.slash }; } @@ -703,7 +712,8 @@ export function create_client({ target, base }) { parent: !!node.uses.parent, route: !!node.uses.route, url: !!node.uses.url - } + }, + slash: node.slash }; } else if (node?.type === 'skip') { return previous ?? null; @@ -998,12 +1008,9 @@ export function create_client({ target, base }) { const params = route.exec(path); if (params) { - const normalized = new URL( - url.origin + normalize_path(url.pathname, trailing_slash) + url.search + url.hash - ); - const id = normalized.pathname + normalized.search; + const id = url.pathname + url.search; /** @type {import('./types').NavigationIntent} */ - const intent = { id, invalidating, route, params: decode_params(params), url: normalized }; + const intent = { id, invalidating, route, params: decode_params(params), url }; return intent; } } diff --git a/packages/kit/src/runtime/client/types.d.ts b/packages/kit/src/runtime/client/types.d.ts index 44a7b8a5d1df..d6e39578f548 100644 --- a/packages/kit/src/runtime/client/types.d.ts +++ b/packages/kit/src/runtime/client/types.d.ts @@ -8,7 +8,7 @@ import { prefetch, prefetchRoutes } from '$app/navigation'; -import { CSRPageNode, CSRPageNodeLoader, CSRRoute, Uses } from 'types'; +import { CSRPageNode, CSRPageNodeLoader, CSRRoute, TrailingSlash, Uses } from 'types'; export interface Client { // public API, exposed via $app/navigation @@ -67,12 +67,14 @@ export type BranchNode = { server: DataNode | null; shared: DataNode | null; data: Record | null; + slash?: TrailingSlash; }; export interface DataNode { type: 'data'; data: Record | null; uses: Uses; + slash?: TrailingSlash; } export interface NavigationState { diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index a79589b9215d..16a214772f80 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -187,8 +187,6 @@ export async function respond(request, options, state) { options.manifest._.nodes[route.page.leaf]() ]); - console.log('page nodes', nodes); - trailing_slash = get_option(nodes, 'trailingSlash'); } else if (route.endpoint) { const node = await route.endpoint(); diff --git a/packages/kit/src/runtime/server/page/load_data.js b/packages/kit/src/runtime/server/page/load_data.js index 899e5aa76332..458d1c8bcbe9 100644 --- a/packages/kit/src/runtime/server/page/load_data.js +++ b/packages/kit/src/runtime/server/page/load_data.js @@ -63,7 +63,8 @@ export async function load_server_data({ event, state, node, parent }) { return { type: 'data', data, - uses + uses, + slash: node.server.trailingSlash }; } diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index e386a8293153..18c6dd8073a3 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -195,7 +195,9 @@ export async function render_response({ if (server_data.uses.route) uses.push(`route:1`); if (server_data.uses.url) uses.push(`url:1`); - return `{type:"data",data:${data},uses:{${uses.join(',')}}}`; + return `{type:"data",data:${data},uses:{${uses.join(',')}}${ + server_data.slash ? `,slash:${s(server_data.slash)}` : '' + }}`; } return s(server_data); diff --git a/packages/kit/src/runtime/server/utils.js b/packages/kit/src/runtime/server/utils.js index 3fe10b903b26..3b9cdf83ee49 100644 --- a/packages/kit/src/runtime/server/utils.js +++ b/packages/kit/src/runtime/server/utils.js @@ -201,5 +201,7 @@ export function serialize_data_node(node) { if (node.uses.route) uses.push(`"route":1`); if (node.uses.url) uses.push(`"url":1`); - return `{"type":"data","data":${stringified},"uses":{${uses.join(',')}}}`; + return `{"type":"data","data":${stringified},"uses":{${uses.join(',')}}${ + node.slash ? `,"slash":${JSON.stringify(node.slash)}` : '' + }}`; } diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index 2fbac254cd8a..2ee921709940 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -1331,7 +1331,7 @@ test.describe('Redirects', () => { }); test.describe('Routing', () => { - test.only('redirects from /routing/ to /routing', async ({ + test('redirects from /routing/ to /routing', async ({ baseURL, page, clicknav, diff --git a/packages/kit/test/apps/options/source/pages/endpoint-with-slash/+server.js b/packages/kit/test/apps/options/source/pages/endpoint-with-slash/+server.js new file mode 100644 index 000000000000..9814536f32df --- /dev/null +++ b/packages/kit/test/apps/options/source/pages/endpoint-with-slash/+server.js @@ -0,0 +1,5 @@ +export function GET() { + return new Response('hi'); +} + +export const trailingSlash = 'always'; diff --git a/packages/kit/test/apps/options/test/test.js b/packages/kit/test/apps/options/test/test.js index be0ea7a1e5d7..149111ca34c8 100644 --- a/packages/kit/test/apps/options/test/test.js +++ b/packages/kit/test/apps/options/test/test.js @@ -151,9 +151,9 @@ test.describe('trailingSlash', () => { expect(await page.textContent('h2')).toBe('/slash/child/'); }); - test('ignores trailing slash on endpoint', async ({ baseURL, request }) => { + test('removes trailing slash on endpoint', async ({ baseURL, request }) => { const r1 = await request.get('/path-base/endpoint/'); - expect(r1.url()).toBe(`${baseURL}/path-base/endpoint/`); + expect(r1.url()).toBe(`${baseURL}/path-base/endpoint`); expect(await r1.text()).toBe('hi'); const r2 = await request.get('/path-base/endpoint'); @@ -161,13 +161,26 @@ test.describe('trailingSlash', () => { expect(await r2.text()).toBe('hi'); }); + test('adds trailing slash to endpoint', async ({ baseURL, request }) => { + const r1 = await request.get('/path-base/endpoint-with-slash'); + expect(r1.url()).toBe(`${baseURL}/path-base/endpoint-with-slash/`); + expect(await r1.text()).toBe('hi'); + + const r2 = await request.get('/path-base/endpoint-with-slash/'); + expect(r2.url()).toBe(`${baseURL}/path-base/endpoint-with-slash/`); + expect(await r2.text()).toBe('hi'); + }); + test('can fetch data from page-endpoint', async ({ request }) => { const r = await request.get('/path-base/page-endpoint/__data.json'); const data = await r.json(); expect(data).toEqual({ type: 'data', - nodes: [null, { type: 'data', data: [{ message: 1 }, 'hi'], uses: {} }] + nodes: [ + { type: 'data', data: [null], uses: {}, slash: 'always' }, + { type: 'data', data: [{ message: 1 }, 'hi'], uses: {} } + ] }); }); diff --git a/packages/kit/test/prerendering/trailing-slash/svelte.config.js b/packages/kit/test/prerendering/trailing-slash/svelte.config.js index a05f200dfaa4..2c7fd6177706 100644 --- a/packages/kit/test/prerendering/trailing-slash/svelte.config.js +++ b/packages/kit/test/prerendering/trailing-slash/svelte.config.js @@ -7,9 +7,7 @@ const config = { kit: { adapter: adapter({ fallback: '200.html' - }), - - trailingSlash: 'always' + }) } }; diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts index 31ed01725e0c..33e084bc3315 100644 --- a/packages/kit/types/internal.d.ts +++ b/packages/kit/types/internal.d.ts @@ -72,6 +72,7 @@ export interface CSRPageNode { component: typeof SvelteComponent; shared: { load?: Load; + trailingSlash?: TrailingSlash; }; server: boolean; } @@ -209,6 +210,7 @@ export interface ServerDataNode { type: 'data'; data: Record | null; uses: Uses; + slash?: TrailingSlash; } /** From 1314f5093f2c0f62fe1668718d8c7afbc1b995c2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 18 Nov 2022 17:54:43 -0500 Subject: [PATCH 3/9] allow SvelteKit to handle redirects --- packages/adapter-vercel/index.js | 78 -------------------------------- 1 file changed, 78 deletions(-) diff --git a/packages/adapter-vercel/index.js b/packages/adapter-vercel/index.js index 6ce0637d4794..89fa8b6ec5c1 100644 --- a/packages/adapter-vercel/index.js +++ b/packages/adapter-vercel/index.js @@ -4,83 +4,6 @@ import { fileURLToPath } from 'url'; import { nodeFileTrace } from '@vercel/nft'; import esbuild from 'esbuild'; -// rules for clean URLs and trailing slash handling, -// generated with @vercel/routing-utils -const redirects = { - always: [ - { - src: '^/(?:(.+)/)?index(?:\\.html)?/?$', - headers: { - Location: '/$1/' - }, - status: 308 - }, - { - src: '^/(.*)\\.html/?$', - headers: { - Location: '/$1/' - }, - status: 308 - }, - { - src: '^/\\.well-known(?:/.*)?$' - }, - { - src: '^/((?:[^/]+/)*[^/\\.]+)$', - headers: { - Location: '/$1/' - }, - status: 308 - }, - { - src: '^/((?:[^/]+/)*[^/]+\\.\\w+)/$', - headers: { - Location: '/$1' - }, - status: 308 - } - ], - never: [ - { - src: '^/(?:(.+)/)?index(?:\\.html)?/?$', - headers: { - Location: '/$1' - }, - status: 308 - }, - { - src: '^/(.*)\\.html/?$', - headers: { - Location: '/$1' - }, - status: 308 - }, - { - src: '^/(.*)/$', - headers: { - Location: '/$1' - }, - status: 308 - } - ], - ignore: [ - { - src: '^/(?:(.+)/)?index(?:\\.html)?/?$', - headers: { - Location: '/$1' - }, - status: 308 - }, - { - src: '^/(.*)\\.html/?$', - headers: { - Location: '/$1' - }, - status: 308 - } - ] -}; - /** @type {import('.').default} **/ export default function ({ external = [], edge, split } = {}) { return { @@ -115,7 +38,6 @@ export default function ({ external = [], edge, split } = {}) { /** @type {any[]} */ const routes = [ - ...redirects[builder.config.kit.trailingSlash], ...prerendered_redirects, { src: `/${builder.getAppPath()}/.+`, From af0603b6ba089f1df878d7a413af982b4fb029c9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 18 Nov 2022 18:08:52 -0500 Subject: [PATCH 4/9] update docs --- .../docs/20-core-concepts/40-page-options.md | 15 +++++++++++++++ .../docs/20-core-concepts/50-adapters.md | 2 +- documentation/docs/40-best-practices/20-seo.md | 2 +- .../docs/50-api-reference/10-configuration.md | 17 ++--------------- packages/adapter-static/README.md | 2 +- packages/kit/src/core/config/options.js | 2 +- 6 files changed, 21 insertions(+), 19 deletions(-) diff --git a/documentation/docs/20-core-concepts/40-page-options.md b/documentation/docs/20-core-concepts/40-page-options.md index f777c6d36271..0ad0160bc438 100644 --- a/documentation/docs/20-core-concepts/40-page-options.md +++ b/documentation/docs/20-core-concepts/40-page-options.md @@ -104,3 +104,18 @@ export const csr = false; ``` > If both `ssr` and `csr` are `false`, nothing will be rendered! + +### trailingSlash + +By default, SvelteKit will remove trailing slashes from URLs — if you visit `/about/`, it will respond with a redirect to `/about`. You can change this behaviour with the `trailingSlash` option, which can be one of `'never'` (the default), `'always'`, or `'ignore'`. + +As with other page options, you can export this value from a `+layout.js` or a `+layout.server.js` and it will apply to all child pages. You can also export the configuration from `+server.js` files. + +```js +/// file: src/routes/+layout.js +export const trailingSlash = 'always'; +``` + +This option also affects [prerendering](#prerender). If `trailingSlash` is `always`, a route like `/about` will result in an `about/index.html` file, otherwise it will create `about.html`, mirroring static webserver conventions. + +> 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. \ No newline at end of file diff --git a/documentation/docs/20-core-concepts/50-adapters.md b/documentation/docs/20-core-concepts/50-adapters.md index 860b1d51d0b4..df14b688ad45 100644 --- a/documentation/docs/20-core-concepts/50-adapters.md +++ b/documentation/docs/20-core-concepts/50-adapters.md @@ -54,7 +54,7 @@ Most adapters will generate static HTML for any [prerenderable](/docs/page-optio You can also use `adapter-static` to generate single-page apps (SPAs) by specifying a [fallback page and disabling SSR](https://github.com/sveltejs/kit/tree/master/packages/adapter-static#spa-mode). -> You must ensure [`trailingSlash`](/docs/configuration#trailingslash) is set appropriately for your environment. If your host does not render `/a.html` upon receiving a request for `/a` then you will need to set `trailingSlash: 'always'` to create `/a/index.html` instead. +> You must ensure [`trailingSlash`](/docs/page-options#trailingslash) is set appropriately for your environment. If your host does not render `/a.html` upon receiving a request for `/a` then you will need to set `trailingSlash: 'always'` to create `/a/index.html` instead. #### Platform-specific context diff --git a/documentation/docs/40-best-practices/20-seo.md b/documentation/docs/40-best-practices/20-seo.md index 5ae9b4333eba..8896af4a5de2 100644 --- a/documentation/docs/40-best-practices/20-seo.md +++ b/documentation/docs/40-best-practices/20-seo.md @@ -18,7 +18,7 @@ Signals such as [Core Web Vitals](https://web.dev/vitals/#core-web-vitals) impac #### Normalized URLs -SvelteKit redirects pathnames with trailing slashes to ones without (or vice versa depending on your [configuration](/docs/configuration#trailingslash)), as duplicate URLs are bad for SEO. +SvelteKit redirects pathnames with trailing slashes to ones without (or vice versa depending on your [configuration](/docs/page-options#trailingslash)), as duplicate URLs are bad for SEO. ### Manual setup diff --git a/documentation/docs/50-api-reference/10-configuration.md b/documentation/docs/50-api-reference/10-configuration.md index dbefdadbdd80..73225538c367 100644 --- a/documentation/docs/50-api-reference/10-configuration.md +++ b/documentation/docs/50-api-reference/10-configuration.md @@ -65,7 +65,6 @@ const config = { register: true, files: (filepath) => !/\.DS_Store/.test(filepath) }, - trailingSlash: 'never', version: { name: Date.now().toString(), pollInterval: 0 @@ -262,7 +261,7 @@ See [Prerendering](/docs/page-options#prerender). An object containing zero or m - `'ignore'` - silently ignore the failure and continue - `'warn'` — continue, but print a warning - `(details) => void` — a custom error handler that takes a `details` object with `status`, `path`, `referrer`, `referenceType` and `message` properties. If you `throw` from this function, the build will fail - + ```js /** @type {import('@sveltejs/kit').Config} */ const config = { @@ -273,7 +272,7 @@ See [Prerendering](/docs/page-options#prerender). An object containing zero or m if (path === '/not-found' && referrer === '/blog/how-we-built-our-404-page') { return; } - + // otherwise fail the build throw new Error(message); } @@ -298,18 +297,6 @@ An object containing zero or more of the following values: - `register` - if set to `false`, will disable automatic service worker registration - `files` - a function with the type of `(filepath: string) => boolean`. When `true`, the given file will be available in `$service-worker.files`, otherwise it will be excluded. -### trailingSlash - -Whether to remove, append, or ignore trailing slashes when resolving URLs (note that this only applies to pages, not endpoints). - -- `'never'` — redirect `/x/` to `/x` -- `'always'` — redirect `/x` to `/x/` -- `'ignore'` — don't automatically add or remove trailing slashes. `/x` and `/x/` will be treated equivalently - -This option also affects [prerendering](/docs/page-options#prerender). If `trailingSlash` is `always`, a route like `/about` will result in an `about/index.html` file, otherwise it will create `about.html`, mirroring static webserver conventions. - -> 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`](/docs/hooks#server-hooks-handle) function. - ### version An object containing zero or more of the following values: diff --git a/packages/adapter-static/README.md b/packages/adapter-static/README.md index f5aa6bd832dd..abb0eb5c0859 100644 --- a/packages/adapter-static/README.md +++ b/packages/adapter-static/README.md @@ -34,7 +34,7 @@ export default { export const prerender = true; ``` -> ⚠️ You must ensure SvelteKit's [`trailingSlash`](https://kit.svelte.dev/docs/configuration#trailingslash) option is set appropriately for your environment. If your host does not render `/a.html` upon receiving a request for `/a` then you will need to set `trailingSlash: 'always'` to create `/a/index.html` instead. +> ⚠️ You must ensure SvelteKit's [`trailingSlash`](https://kit.svelte.dev/docs/page-options#trailingslash) option is set appropriately for your environment. If your host does not render `/a.html` upon receiving a request for `/a` then you will need to set `trailingSlash: 'always'` to create `/a/index.html` instead. ## Zero-config support diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js index 2288be6da306..828280556b62 100644 --- a/packages/kit/src/core/config/options.js +++ b/packages/kit/src/core/config/options.js @@ -232,7 +232,7 @@ const options = object( crawl: boolean(true), createIndexFiles: error( (keypath) => - `${keypath} has been removed — it is now controlled by the trailingSlash option. See https://kit.svelte.dev/docs/configuration#trailingslash` + `${keypath} has been removed — it is now controlled by the trailingSlash option. See https://kit.svelte.dev/docs/page-options#trailingslash` ), default: error( (keypath) => From 961aa70a0fb668bdba832c509227d0479aeed317 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 18 Nov 2022 18:20:30 -0500 Subject: [PATCH 5/9] simplify tests --- packages/kit/src/runtime/server/index.js | 4 +-- .../src/routes/trailing-slash}/+layout.js | 1 - .../src/routes/trailing-slash}/+layout.svelte | 0 .../src/routes/trailing-slash}/+page.svelte | 0 .../trailing-slash}/page/+page.server.js | 0 .../routes/trailing-slash}/page/+page.svelte | 0 .../standalone-endpoint.json/+server.js | 0 .../prerendering/basics/src/service-worker.js | 4 +++ .../kit/test/prerendering/basics/test/test.js | 12 +++++++++ .../prerendering/trailing-slash/package.json | 21 ---------------- .../prerendering/trailing-slash/src/app.d.ts | 1 - .../prerendering/trailing-slash/src/app.html | 12 --------- .../trailing-slash/src/hooks.server.js | 0 .../trailing-slash/src/service-worker.js | 3 --- .../trailing-slash/static/favicon.png | Bin 1571 -> 0 bytes .../trailing-slash/svelte.config.js | 14 ----------- .../prerendering/trailing-slash/test/test.js | 19 --------------- .../prerendering/trailing-slash/tsconfig.json | 16 ------------ .../trailing-slash/vite.config.js | 23 ------------------ 19 files changed, 18 insertions(+), 112 deletions(-) rename packages/kit/test/prerendering/{trailing-slash/src/routes => basics/src/routes/trailing-slash}/+layout.js (55%) rename packages/kit/test/prerendering/{trailing-slash/src/routes => basics/src/routes/trailing-slash}/+layout.svelte (100%) rename packages/kit/test/prerendering/{trailing-slash/src/routes => basics/src/routes/trailing-slash}/+page.svelte (100%) rename packages/kit/test/prerendering/{trailing-slash/src/routes => basics/src/routes/trailing-slash}/page/+page.server.js (100%) rename packages/kit/test/prerendering/{trailing-slash/src/routes => basics/src/routes/trailing-slash}/page/+page.svelte (100%) rename packages/kit/test/prerendering/{trailing-slash/src/routes => basics/src/routes/trailing-slash}/standalone-endpoint.json/+server.js (100%) delete mode 100644 packages/kit/test/prerendering/trailing-slash/package.json delete mode 100644 packages/kit/test/prerendering/trailing-slash/src/app.d.ts delete mode 100644 packages/kit/test/prerendering/trailing-slash/src/app.html delete mode 100644 packages/kit/test/prerendering/trailing-slash/src/hooks.server.js delete mode 100644 packages/kit/test/prerendering/trailing-slash/src/service-worker.js delete mode 100644 packages/kit/test/prerendering/trailing-slash/static/favicon.png delete mode 100644 packages/kit/test/prerendering/trailing-slash/svelte.config.js delete mode 100644 packages/kit/test/prerendering/trailing-slash/test/test.js delete mode 100644 packages/kit/test/prerendering/trailing-slash/tsconfig.json delete mode 100644 packages/kit/test/prerendering/trailing-slash/vite.config.js diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 16a214772f80..18a9fe003395 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -92,8 +92,6 @@ export async function respond(request, options, state) { /** @type {Record} */ const headers = {}; - if (state.prerendering && !state.prerendering.fallback) disable_search(url); - /** @type {import('types').RequestEvent} */ const event = { // @ts-expect-error `cookies` and `fetch` need to be created after the `event` itself @@ -219,6 +217,8 @@ export async function respond(request, options, state) { event.cookies = cookies; event.fetch = create_fetch({ event, options, state, get_cookie_header }); + if (state.prerendering && !state.prerendering.fallback) disable_search(url); + const response = await options.hooks.handle({ event, resolve: (event, opts) => diff --git a/packages/kit/test/prerendering/trailing-slash/src/routes/+layout.js b/packages/kit/test/prerendering/basics/src/routes/trailing-slash/+layout.js similarity index 55% rename from packages/kit/test/prerendering/trailing-slash/src/routes/+layout.js rename to packages/kit/test/prerendering/basics/src/routes/trailing-slash/+layout.js index ba58d86071e4..d3c325085ed2 100644 --- a/packages/kit/test/prerendering/trailing-slash/src/routes/+layout.js +++ b/packages/kit/test/prerendering/basics/src/routes/trailing-slash/+layout.js @@ -1,2 +1 @@ -export const prerender = true; export const trailingSlash = 'always'; diff --git a/packages/kit/test/prerendering/trailing-slash/src/routes/+layout.svelte b/packages/kit/test/prerendering/basics/src/routes/trailing-slash/+layout.svelte similarity index 100% rename from packages/kit/test/prerendering/trailing-slash/src/routes/+layout.svelte rename to packages/kit/test/prerendering/basics/src/routes/trailing-slash/+layout.svelte diff --git a/packages/kit/test/prerendering/trailing-slash/src/routes/+page.svelte b/packages/kit/test/prerendering/basics/src/routes/trailing-slash/+page.svelte similarity index 100% rename from packages/kit/test/prerendering/trailing-slash/src/routes/+page.svelte rename to packages/kit/test/prerendering/basics/src/routes/trailing-slash/+page.svelte diff --git a/packages/kit/test/prerendering/trailing-slash/src/routes/page/+page.server.js b/packages/kit/test/prerendering/basics/src/routes/trailing-slash/page/+page.server.js similarity index 100% rename from packages/kit/test/prerendering/trailing-slash/src/routes/page/+page.server.js rename to packages/kit/test/prerendering/basics/src/routes/trailing-slash/page/+page.server.js diff --git a/packages/kit/test/prerendering/trailing-slash/src/routes/page/+page.svelte b/packages/kit/test/prerendering/basics/src/routes/trailing-slash/page/+page.svelte similarity index 100% rename from packages/kit/test/prerendering/trailing-slash/src/routes/page/+page.svelte rename to packages/kit/test/prerendering/basics/src/routes/trailing-slash/page/+page.svelte diff --git a/packages/kit/test/prerendering/trailing-slash/src/routes/standalone-endpoint.json/+server.js b/packages/kit/test/prerendering/basics/src/routes/trailing-slash/standalone-endpoint.json/+server.js similarity index 100% rename from packages/kit/test/prerendering/trailing-slash/src/routes/standalone-endpoint.json/+server.js rename to packages/kit/test/prerendering/basics/src/routes/trailing-slash/standalone-endpoint.json/+server.js diff --git a/packages/kit/test/prerendering/basics/src/service-worker.js b/packages/kit/test/prerendering/basics/src/service-worker.js index fd6a6580a6fa..ae0028371d35 100644 --- a/packages/kit/test/prerendering/basics/src/service-worker.js +++ b/packages/kit/test/prerendering/basics/src/service-worker.js @@ -1,3 +1,7 @@ +import { prerendered } from '$service-worker'; + +console.log(prerendered); + if (process.env.MY_ENV === 'MY_ENV DEFINED') { console.log(process.env.MY_ENV); } diff --git a/packages/kit/test/prerendering/basics/test/test.js b/packages/kit/test/prerendering/basics/test/test.js index 4e9c79db4bae..f4bf390fdf9e 100644 --- a/packages/kit/test/prerendering/basics/test/test.js +++ b/packages/kit/test/prerendering/basics/test/test.js @@ -230,4 +230,16 @@ test('define service worker variables', () => { assert.ok(content.includes(`MY_ENV DEFINED`)); }); +test('prerendered.paths omits trailing slashes for endpoints', () => { + const content = read('service-worker.js'); + + for (const path of [ + '/trailing-slash/page/', + '/trailing-slash/page/__data.json', + '/trailing-slash/standalone-endpoint.json' + ]) { + assert.ok(content.includes(`"${path}"`), `Missing ${path}`); + } +}); + test.run(); diff --git a/packages/kit/test/prerendering/trailing-slash/package.json b/packages/kit/test/prerendering/trailing-slash/package.json deleted file mode 100644 index 05946eb8122d..000000000000 --- a/packages/kit/test/prerendering/trailing-slash/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "prerendering-test-trailing-slash", - "private": true, - "version": "0.0.1", - "scripts": { - "dev": "vite dev", - "build": "vite build", - "preview": "vite preview", - "check": "svelte-kit sync && tsc && svelte-check", - "test": "svelte-kit sync && pnpm build && uvu test" - }, - "devDependencies": { - "@sveltejs/kit": "workspace:*", - "svelte": "^3.52.0", - "svelte-check": "^2.9.2", - "typescript": "^4.8.4", - "uvu": "^0.5.6", - "vite": "^3.2.1" - }, - "type": "module" -} diff --git a/packages/kit/test/prerendering/trailing-slash/src/app.d.ts b/packages/kit/test/prerendering/trailing-slash/src/app.d.ts deleted file mode 100644 index 7b39ff31cd34..000000000000 --- a/packages/kit/test/prerendering/trailing-slash/src/app.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// ; diff --git a/packages/kit/test/prerendering/trailing-slash/src/app.html b/packages/kit/test/prerendering/trailing-slash/src/app.html deleted file mode 100644 index cee411bbea8f..000000000000 --- a/packages/kit/test/prerendering/trailing-slash/src/app.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - %sveltekit.head% - - - %sveltekit.body% - - diff --git a/packages/kit/test/prerendering/trailing-slash/src/hooks.server.js b/packages/kit/test/prerendering/trailing-slash/src/hooks.server.js deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/kit/test/prerendering/trailing-slash/src/service-worker.js b/packages/kit/test/prerendering/trailing-slash/src/service-worker.js deleted file mode 100644 index 793ee4f3270c..000000000000 --- a/packages/kit/test/prerendering/trailing-slash/src/service-worker.js +++ /dev/null @@ -1,3 +0,0 @@ -import { prerendered } from '$service-worker'; - -console.log(prerendered); diff --git a/packages/kit/test/prerendering/trailing-slash/static/favicon.png b/packages/kit/test/prerendering/trailing-slash/static/favicon.png deleted file mode 100644 index 825b9e65af7c104cfb07089bb28659393b4f2097..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1571 zcmV+;2Hg3HP)Px)-AP12RCwC$UE6KzI1p6{F2N z1VK2vi|pOpn{~#djwYcWXTI_im_u^TJgMZ4JMOsSj!0ma>B?-(Hr@X&W@|R-$}W@Z zgj#$x=!~7LGqHW?IO8+*oE1MyDp!G=L0#^lUx?;!fXv@l^6SvTnf^ac{5OurzC#ZMYc20lI%HhX816AYVs1T3heS1*WaWH z%;x>)-J}YB5#CLzU@GBR6sXYrD>Vw(Fmt#|JP;+}<#6b63Ike{Fuo!?M{yEffez;| zp!PfsuaC)>h>-AdbnwN13g*1LowNjT5?+lFVd#9$!8Z9HA|$*6dQ8EHLu}U|obW6f z2%uGv?vr=KNq7YYa2Roj;|zooo<)lf=&2yxM@e`kM$CmCR#x>gI>I|*Ubr({5Y^rb zghxQU22N}F51}^yfDSt786oMTc!W&V;d?76)9KXX1 z+6Okem(d}YXmmOiZq$!IPk5t8nnS{%?+vDFz3BevmFNgpIod~R{>@#@5x9zJKEHLHv!gHeK~n)Ld!M8DB|Kfe%~123&Hz1Z(86nU7*G5chmyDe ziV7$pB7pJ=96hpxHv9rCR29%bLOXlKU<_13_M8x)6;P8E1Kz6G<&P?$P^%c!M5`2` zfY2zg;VK5~^>TJGQzc+33-n~gKt{{of8GzUkWmU110IgI0DLxRIM>0US|TsM=L|@F z0Bun8U!cRB7-2apz=y-7*UxOxz@Z0)@QM)9wSGki1AZ38ceG7Q72z5`i;i=J`ILzL z@iUO?SBBG-0cQuo+an4TsLy-g-x;8P4UVwk|D8{W@U1Zi z!M)+jqy@nQ$p?5tsHp-6J304Q={v-B>66$P0IDx&YT(`IcZ~bZfmn11#rXd7<5s}y zBi9eim&zQc0Dk|2>$bs0PnLmDfMP5lcXRY&cvJ=zKxI^f0%-d$tD!`LBf9^jMSYUA zI8U?CWdY@}cRq6{5~y+)#h1!*-HcGW@+gZ4B};0OnC~`xQOyH19z*TA!!BJ%9s0V3F?CAJ{hTd#*tf+ur-W9MOURF-@B77_-OshsY}6 zOXRY=5%C^*26z?l)1=$bz30!so5tfABdSYzO+H=CpV~aaUefmjvfZ3Ttu9W&W3Iu6 zROlh0MFA5h;my}8lB0tAV-Rvc2Zs_CCSJnx@d`**$idgy-iMob4dJWWw|21b4NB=LfsYp0Aeh{Ov)yztQi;eL4y5 zMi>8^SzKqk8~k?UiQK^^-5d8c%bV?$F8%X~czyiaKCI2=UH fs.readFileSync(`${build}/${file}`, 'utf-8'); - -test('prerendered.paths omits trailing slashes for endpoints', () => { - const content = read('service-worker.js'); - - for (const path of ['/page/', '/page/__data.json', '/standalone-endpoint.json']) { - assert.ok(content.includes(`"${path}"`), `Missing ${path}`); - } -}); - -test.run(); diff --git a/packages/kit/test/prerendering/trailing-slash/tsconfig.json b/packages/kit/test/prerendering/trailing-slash/tsconfig.json deleted file mode 100644 index df547741d5bb..000000000000 --- a/packages/kit/test/prerendering/trailing-slash/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "noEmit": true, - "module": "esnext", - "moduleResolution": "node", - "paths": { - "@sveltejs/kit": ["../../../types"], - "$lib": ["./src/lib"], - "$lib/*": ["./src/lib/*"], - "types": ["../../../types/internal"] - } - }, - "extends": "./.svelte-kit/tsconfig.json" -} diff --git a/packages/kit/test/prerendering/trailing-slash/vite.config.js b/packages/kit/test/prerendering/trailing-slash/vite.config.js deleted file mode 100644 index a2fe148d646b..000000000000 --- a/packages/kit/test/prerendering/trailing-slash/vite.config.js +++ /dev/null @@ -1,23 +0,0 @@ -import * as path from 'path'; -import { sveltekit } from '@sveltejs/kit/vite'; - -/** @type {import('vite').UserConfig} */ -const config = { - build: { - minify: false - }, - - clearScreen: false, - - logLevel: 'silent', - - plugins: [sveltekit()], - - server: { - fs: { - allow: [path.resolve('../../../src')] - } - } -}; - -export default config; From 93b4d4aeb44bc4051b1200453238bf99b6cbb469 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 18 Nov 2022 18:21:30 -0500 Subject: [PATCH 6/9] replace TODO url --- packages/kit/src/core/config/options.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js index 828280556b62..ab6fae2d50c7 100644 --- a/packages/kit/src/core/config/options.js +++ b/packages/kit/src/core/config/options.js @@ -345,7 +345,7 @@ const options = object( trailingSlash: error( (keypath, input) => - `${keypath} has been removed. You can set \`export const trailingSlash = '${input}'\` inside a top level +layout.js (or +layout.server.js) instead. See the PR for more information: https://github.com/sveltejs/kit/pull/TODO` + `${keypath} has been removed. You can set \`export const trailingSlash = '${input}'\` inside a top level +layout.js (or +layout.server.js) instead. See the PR for more information: https://github.com/sveltejs/kit/pull/7719` ), version: object({ From 3640f8bee5bc1afe7d36766a8c4c21bf9f29ee58 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 18 Nov 2022 18:22:36 -0500 Subject: [PATCH 7/9] changesets --- .changeset/curly-feet-join.md | 5 +++++ .changeset/four-dots-compare.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/curly-feet-join.md create mode 100644 .changeset/four-dots-compare.md diff --git a/.changeset/curly-feet-join.md b/.changeset/curly-feet-join.md new file mode 100644 index 000000000000..275e1f27a7ac --- /dev/null +++ b/.changeset/curly-feet-join.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-vercel': patch +--- + +Handle redirects inside SvelteKit diff --git a/.changeset/four-dots-compare.md b/.changeset/four-dots-compare.md new file mode 100644 index 000000000000..f933f3608a4a --- /dev/null +++ b/.changeset/four-dots-compare.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +[breaking] Make `trailingSlash` a page option, rather than configuration From d1793789a475461eaac62623a07caf48c7ba80a0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 18 Nov 2022 18:23:24 -0500 Subject: [PATCH 8/9] lint --- packages/kit/test/prerendering/options/svelte.config.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/kit/test/prerendering/options/svelte.config.js b/packages/kit/test/prerendering/options/svelte.config.js index 2fea28fb6f60..3409b9e7e972 100644 --- a/packages/kit/test/prerendering/options/svelte.config.js +++ b/packages/kit/test/prerendering/options/svelte.config.js @@ -20,9 +20,7 @@ const config = { paths: { base: '/path-base', assets: 'https://cdn.example.com/stuff' - }, - - trailingSlash: 'always' + } } }; From 0b5da1b99d972add6f150f22f747a0c70a9d0a5c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 18 Nov 2022 19:46:16 -0500 Subject: [PATCH 9/9] update lockfile --- pnpm-lock.yaml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 300f31dff5b2..6c5bc1d28541 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -640,22 +640,6 @@ importers: uvu: 0.5.6 vite: 3.2.3 - packages/kit/test/prerendering/trailing-slash: - specifiers: - '@sveltejs/kit': workspace:* - svelte: ^3.52.0 - svelte-check: ^2.9.2 - typescript: ^4.8.4 - uvu: ^0.5.6 - vite: ^3.2.1 - devDependencies: - '@sveltejs/kit': link:../../.. - svelte: 3.53.1 - svelte-check: 2.9.2_svelte@3.53.1 - typescript: 4.8.4 - uvu: 0.5.6 - vite: 3.2.3 - packages/migrate: specifiers: '@types/prompts': ^2.4.1