Skip to content

Commit

Permalink
[feat] allow +server.js files next to +page files
Browse files Browse the repository at this point in the history
Closes #5896
  • Loading branch information
dummdidumm committed Sep 13, 2022
1 parent e20425f commit 95c9eef
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 9 deletions.
5 changes: 5 additions & 0 deletions .changeset/lazy-mice-remain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

[feat] allow +server.js files next to +page files
34 changes: 34 additions & 0 deletions documentation/docs/06-form-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,3 +349,37 @@ We can also implement progressive enhancement ourselves, without `use:enhance`,
<!-- content -->
</form>
```

### 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
<script>
let result;
async function update() {
const response = await fetch('/crud', {
method: 'PUT',
body: JSON.stringify({ new: 'value' }),
'content-type': 'application/json',
'accept': 'application/json'
});
result = await response.json();
}
</script>
<button on:click={update}>Update</button>
<p>Result: {JSON.stringify(result)}</p>
```

```js
/// file: src/routes/crud/+server.js
import { json } from '@sveltejs/kit';

/** @type {import('./$types').RequestHandler} */
export function PUT() {
// ...
return json({ new: 'value' });
}
```
5 changes: 0 additions & 5 deletions packages/kit/src/core/sync/create_manifest_data/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
Expand Down
22 changes: 22 additions & 0 deletions packages/kit/src/runtime/server/endpoint.js
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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;
}
6 changes: 3 additions & 3 deletions packages/kit/src/runtime/server/index.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script>
let result;
/** @param {string} method */
async function request(method) {
result = 'loading';
const response = await fetch('/routing/endpoint-next-to-page', {method});
result = await response.text();
}
</script>

<p>Hi</p>
<button on:click={() => request('GET')}>GET</button>
<button on:click={() => request('PUT')}>PUT</button>
<button on:click={() => request('PATCH')}>PATCH</button>
<button on:click={() => request('POST')}>POST</button>
<button on:click={() => request('DELETE')}>DELETE</button>
<pre>{result}</pre>
Original file line number Diff line number Diff line change
@@ -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');
}
22 changes: 21 additions & 1 deletion packages/kit/test/apps/basics/test/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"]');
Expand Down Expand Up @@ -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');
});

0 comments on commit 95c9eef

Please sign in to comment.