From 95c9eefd032386765151f7a3c4cc56ee35845929 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Tue, 13 Sep 2022 16:48:16 +0200 Subject: [PATCH] [feat] allow +server.js files next to +page files Closes #5896 --- .changeset/lazy-mice-remain.md | 5 +++ documentation/docs/06-form-actions.md | 34 +++++++++++++++++++ .../core/sync/create_manifest_data/index.js | 5 --- packages/kit/src/runtime/server/endpoint.js | 22 ++++++++++++ packages/kit/src/runtime/server/index.js | 6 ++-- .../endpoint-next-to-page/+page.svelte | 18 ++++++++++ .../routing/endpoint-next-to-page/+server.js | 24 +++++++++++++ .../kit/test/apps/basics/test/client.test.js | 22 +++++++++++- 8 files changed, 127 insertions(+), 9 deletions(-) create mode 100644 .changeset/lazy-mice-remain.md create mode 100644 packages/kit/test/apps/basics/src/routes/routing/endpoint-next-to-page/+page.svelte create mode 100644 packages/kit/test/apps/basics/src/routes/routing/endpoint-next-to-page/+server.js diff --git a/.changeset/lazy-mice-remain.md b/.changeset/lazy-mice-remain.md new file mode 100644 index 000000000000..519d0d7fd63d --- /dev/null +++ b/.changeset/lazy-mice-remain.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +[feat] allow +server.js files next to +page files diff --git a/documentation/docs/06-form-actions.md b/documentation/docs/06-form-actions.md index 32eae3d119ad..64d1da90e779 100644 --- a/documentation/docs/06-form-actions.md +++ b/documentation/docs/06-form-actions.md @@ -349,3 +349,37 @@ We can also implement progressive enhancement ourselves, without `use:enhance`, ``` + +### Alternatives + +Form actions are specifically designed to also work without JavaScript, which means you need to use forms for it. If you don't need the page to work without JavaScript and/or you for example want to interact with an API through JSON, you can instead use `+server.js` files: + +```svelte +/// file: src/routes/crud/+page.svelte + + + +

Result: {JSON.stringify(result)}

+``` + +```js +/// file: src/routes/crud/+server.js +import { json } from '@sveltejs/kit'; + +/** @type {import('./$types').RequestHandler} */ +export function PUT() { + // ... + return json({ new: 'value' }); +} +``` diff --git a/packages/kit/src/core/sync/create_manifest_data/index.js b/packages/kit/src/core/sync/create_manifest_data/index.js index c386bf1ed6f1..563c391b02ab 100644 --- a/packages/kit/src/core/sync/create_manifest_data/index.js +++ b/packages/kit/src/core/sync/create_manifest_data/index.js @@ -272,11 +272,6 @@ function create_routes_and_nodes(cwd, config, fallback) { route_map.forEach((route) => { if (!route.leaf) return; - if (route.leaf && route.endpoint) { - // TODO possibly relax this https://github.com/sveltejs/kit/issues/5896 - throw new Error(`${route.endpoint.file} cannot share a directory with other route files`); - } - route.page = { layouts: [], errors: [], diff --git a/packages/kit/src/runtime/server/endpoint.js b/packages/kit/src/runtime/server/endpoint.js index 5e3aba0840e4..964ce834b59b 100644 --- a/packages/kit/src/runtime/server/endpoint.js +++ b/packages/kit/src/runtime/server/endpoint.js @@ -1,4 +1,5 @@ import { json } from '../../exports/index.js'; +import { negotiate } from '../../utils/http.js'; import { Redirect, ValidationError } from '../control.js'; import { check_method_names, method_not_allowed } from './utils.js'; @@ -64,3 +65,24 @@ export async function render_endpoint(event, mod, state) { throw error; } } + +/** + * @param {import('types').RequestEvent} event + */ +export function is_endpoint_request(event) { + const result = + // These only exist for +server + ['PUT', 'PATCH', 'DELETE'].includes(event.request.method) || + // GET has accept text/html for pages + (event.request.method === 'GET' && + negotiate(event.request.headers.get('accept') ?? '*/*', ['application/json', 'text/html']) === + 'application/json') || + // POST with FormData is for actions + (event.request.method === 'POST' && + !(event.request.headers.get('content-type') ?? '') + .split(';') + .some((part) => + ['multipart/form-data', 'application/x-www-form-urlencoded'].includes(part.trim()) + )); + return result; +} diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index e12128c9011a..7c2f495dcfaa 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -1,5 +1,5 @@ import * as cookie from 'cookie'; -import { render_endpoint } from './endpoint.js'; +import { is_endpoint_request, render_endpoint } from './endpoint.js'; import { render_page } from './page/index.js'; import { render_response } from './page/render.js'; import { respond_with_error } from './page/respond_with_error.js'; @@ -227,10 +227,10 @@ export async function respond(request, options, state) { if (is_data_request) { response = await render_data(event, route, options, state); + } else if (route.endpoint && (!route.page || is_endpoint_request(event))) { + response = await render_endpoint(event, await route.endpoint(), state); } else if (route.page) { response = await render_page(event, route, route.page, options, state, resolve_opts); - } else if (route.endpoint) { - response = await render_endpoint(event, await route.endpoint(), state); } else { // a route will always have a page or an endpoint, but TypeScript // doesn't know that diff --git a/packages/kit/test/apps/basics/src/routes/routing/endpoint-next-to-page/+page.svelte b/packages/kit/test/apps/basics/src/routes/routing/endpoint-next-to-page/+page.svelte new file mode 100644 index 000000000000..b88ab6af7f8f --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/routing/endpoint-next-to-page/+page.svelte @@ -0,0 +1,18 @@ + + +

Hi

+ + + + + +
{result}
diff --git a/packages/kit/test/apps/basics/src/routes/routing/endpoint-next-to-page/+server.js b/packages/kit/test/apps/basics/src/routes/routing/endpoint-next-to-page/+server.js new file mode 100644 index 000000000000..007c90f0d3ed --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/routing/endpoint-next-to-page/+server.js @@ -0,0 +1,24 @@ +/** @type {import('./$types').RequestHandler} */ +export function GET() { + return new Response('GET'); +} + +/** @type {import('./$types').RequestHandler} */ +export function PUT() { + return new Response('PUT'); +} + +/** @type {import('./$types').RequestHandler} */ +export function PATCH() { + return new Response('PATCH'); +} + +/** @type {import('./$types').RequestHandler} */ +export function POST() { + return new Response('POST'); +} + +/** @type {import('./$types').RequestHandler} */ +export function DELETE() { + return new Response('DELETE'); +} diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js index 1c12378205a8..ed55b7ae27e7 100644 --- a/packages/kit/test/apps/basics/test/client.test.js +++ b/packages/kit/test/apps/basics/test/client.test.js @@ -8,7 +8,7 @@ test.skip(({ javaScriptEnabled }) => !javaScriptEnabled); test.describe.configure({ mode: 'parallel' }); test.describe('beforeNavigate', () => { - test('prevents navigation triggered by link click', async ({ clicknav, page, baseURL }) => { + test('prevents navigation triggered by link click', async ({ page, baseURL }) => { await page.goto('/before-navigate/prevent-navigation'); await page.click('[href="/before-navigate/a"]'); @@ -785,3 +785,23 @@ test.describe('data-sveltekit attributes', () => { expect(await page.evaluate(() => window.scrollY)).toBe(0); }); }); + +test('+server.js next to +page.svelte works', async ({ page }) => { + await page.goto('/routing/endpoint-next-to-page'); + expect(await page.textContent('p')).toBe('Hi'); + + await page.click('button:has-text("GET")'); + await expect(page.locator('pre')).toHaveText('GET'); + + await page.click('button:has-text("PUT")'); + await expect(page.locator('pre')).toHaveText('PUT'); + + await page.click('button:has-text("PATCH")'); + await expect(page.locator('pre')).toHaveText('PATCH'); + + await page.click('button:has-text("POST")'); + await expect(page.locator('pre')).toHaveText('POST'); + + await page.click('button:has-text("DELETE")'); + await expect(page.locator('pre')).toHaveText('DELETE'); +});