Skip to content

Commit c2d33b1

Browse files
committed
Merge branch 'main' into version-3
2 parents 1938a64 + aa466ec commit c2d33b1

27 files changed

Lines changed: 238 additions & 124 deletions

File tree

.changeset/silent-sites-end.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
fix: `config.kit.csp.directives['trusted-types']` requires `'svelte-trusted-html'` (and `'sveltekit-trusted-url'` when a service worker is automatically registered) if it is configured

.changeset/twelve-taxis-move.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': minor
3+
---
4+
5+
feat: return boolean from `submit` to indicate submission validity for enhanced `form` remote functions

CONTRIBUTING.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,20 @@ If there are tests that fail on the CI, you can retrieve the failed screenshots
8484

8585
It is very easy to introduce flakiness in a browser test. If you try to fix the flakiness in a test, you can run it until failure to gain some confidence you've fixed the test with a command like:
8686

87+
```sh
88+
pnpx playwright test --workers=1 --repeat-each 1000 --max-failures 1 -g "accepts a Request object"
8789
```
88-
npx playwright test --workers=1 --repeat-each 1000 --max-failures 1 -g "accepts a Request object"
90+
91+
The Playwright tests can also be run with the following environment variables to augment how the tests run locally:
92+
93+
```sh
94+
# Default values
95+
KIT_E2E_BROWSER=chromium
96+
KIT_E2E_RETRIES=0
97+
KIT_E2E_WORKERS=undefined
98+
99+
# Append the modified environment variable before the test command
100+
KIT_E2E_RETRIES=2 pnpm test:kit
89101
```
90102

91103
## Working on Vite and other dependencies

documentation/docs/20-core-concepts/60-remote-functions.md

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -392,13 +392,22 @@ Because our form contains a `file` input, we've added an `enctype="multipart/for
392392
393393
In the case of `radio` and `checkbox` inputs that all belong to the same field, the `value` must be specified as a second argument to `.as(...)`:
394394
395+
```js
396+
/// file: constants.js
397+
export const operatingSystems = /** @type {const} */ (['windows', 'mac', 'linux']);
398+
export const languages = /** @type {const} */ (['html', 'css', 'js']);
399+
```
400+
395401
```js
396402
/// file: data.remote.js
403+
// @filename: constants.js
404+
export const operatingSystems = /** @type {const} */ (['windows', 'mac', 'linux']);
405+
export const languages = /** @type {const} */ (['html', 'css', 'js']);
406+
// @filename: index.js
397407
import * as v from 'valibot';
398408
import { form } from '$app/server';
399409
// ---cut---
400-
export const operatingSystems = /** @type {const} */ (['windows', 'mac', 'linux']);
401-
export const languages = /** @type {const} */ (['html', 'css', 'js']);
410+
import { operatingSystems, languages } from './constants';
402411

403412
export const survey = form(
404413
v.object({
@@ -704,10 +713,13 @@ We can customize what happens when the form is submitted with the `enhance` meth
704713

705714
<form {...createPost.enhance(async ({ form, data, submit }) => {
706715
try {
707-
await submit();
708-
form.reset();
716+
if (await submit()) {
717+
form.reset();
709718

710-
showToast('Successfully published!');
719+
showToast('Successfully published!');
720+
} else {
721+
showToast('Invalid data!');
722+
}
711723
} catch (error) {
712724
showToast('Oh no! Something went wrong');
713725
}

packages/kit/src/core/config/index.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import fs from 'node:fs';
22
import path from 'node:path';
3+
import process from 'node:process';
34
import * as url from 'node:url';
45
import options from './options.js';
6+
import { resolve_entry } from '../../utils/filesystem.js';
57

68
/**
79
* Loads the template (src/app.html by default) and validates that it has the
@@ -96,7 +98,7 @@ export async function load_config({ cwd }) {
9698
* @returns {import('types').ValidatedConfig}
9799
*/
98100
export function process_config(config, { cwd }) {
99-
const validated = validate_config(config);
101+
const validated = validate_config(config, cwd);
100102

101103
validated.kit.env.dir = path.resolve(cwd, validated.kit.env.dir);
102104
validated.kit.outDir = path.resolve(cwd, validated.kit.outDir);
@@ -117,15 +119,17 @@ export function process_config(config, { cwd }) {
117119

118120
/**
119121
* @param {import('@sveltejs/kit').Config} config
122+
* @param {string} [cwd]
120123
* @returns {import('types').ValidatedConfig}
121124
*/
122-
export function validate_config(config) {
125+
export function validate_config(config, cwd = process.cwd()) {
123126
if (typeof config !== 'object') {
124127
throw new Error(
125128
'The Svelte config file must have a configuration object as its default export. See https://svelte.dev/docs/kit/configuration'
126129
);
127130
}
128131

132+
/** @type {import('types').ValidatedConfig} */
129133
const validated = options(config, 'config');
130134
const files = validated.kit.files;
131135

@@ -152,5 +156,22 @@ export function validate_config(config) {
152156
}
153157
}
154158

159+
if (validated.kit.csp?.directives?.['require-trusted-types-for']?.includes('script')) {
160+
if (!validated.kit.csp?.directives?.['trusted-types']?.includes('svelte-trusted-html')) {
161+
throw new Error(
162+
"The `csp.directives['trusted-types']` option must include 'svelte-trusted-html'"
163+
);
164+
}
165+
if (
166+
validated.kit.serviceWorker?.register &&
167+
resolve_entry(path.resolve(cwd, validated.kit.files.serviceWorker)) &&
168+
!validated.kit.csp?.directives?.['trusted-types']?.includes('sveltekit-trusted-url')
169+
) {
170+
throw new Error(
171+
"The `csp.directives['trusted-types']` option must include 'sveltekit-trusted-url' when `serviceWorker.register` is true"
172+
);
173+
}
174+
}
175+
155176
return validated;
156177
}

packages/kit/src/core/config/options.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import process from 'node:process';
1+
/** @import { Validator } from './types.js' */
22

3-
/** @typedef {import('./types.js').Validator} Validator */
3+
import process from 'node:process';
44

55
const directives = object({
66
'child-src': string_array(),

packages/kit/src/exports/public.d.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2040,10 +2040,10 @@ export type RemoteForm<Input extends RemoteFormInput | void, Output> = {
20402040
callback: (opts: {
20412041
form: HTMLFormElement;
20422042
data: Input;
2043-
submit: () => Promise<void> & {
2044-
updates: (...updates: RemoteQueryUpdate[]) => Promise<void>;
2043+
submit: () => Promise<boolean> & {
2044+
updates: (...updates: RemoteQueryUpdate[]) => Promise<boolean>;
20452045
};
2046-
}) => void | Promise<void>
2046+
}) => void
20472047
): {
20482048
method: 'POST';
20492049
action: string;

packages/kit/src/runtime/client/remote-functions/form.svelte.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ export function form(id) {
130130
}
131131

132132
try {
133+
// eslint-disable-next-line @typescript-eslint/await-thenable -- `callback` is typed as returning `void` to allow returning e.g. `Promise<boolean>`
133134
await callback({
134135
form,
135136
data,
@@ -146,7 +147,7 @@ export function form(id) {
146147

147148
/**
148149
* @param {FormData} data
149-
* @returns {Promise<any> & { updates: (...args: any[]) => any }}
150+
* @returns {Promise<boolean> & { updates: (...args: any[]) => Promise<boolean> }}
150151
*/
151152
function submit(data) {
152153
// Store a reference to the current instance and increment the usage count for the duration
@@ -167,7 +168,7 @@ export function form(id) {
167168
/** @type {Error | undefined} */
168169
let updates_error;
169170

170-
/** @type {Promise<any> & { updates: (...args: RemoteQueryUpdate[]) => Promise<any> }} */
171+
/** @type {Promise<boolean> & { updates: (...args: RemoteQueryUpdate[]) => Promise<boolean> }} */
171172
const promise = (async () => {
172173
try {
173174
await Promise.resolve();
@@ -204,21 +205,25 @@ export function form(id) {
204205

205206
if (form_result.type === 'result') {
206207
({ issues: raw_issues = [], result } = devalue.parse(form_result.result, app.decoders));
208+
const succeeded = raw_issues.length === 0;
207209

208-
if (!issues.$) {
210+
if (succeeded) {
209211
if (form_result.refreshes) {
210212
apply_refreshes(form_result.refreshes);
211213
} else {
212214
void invalidateAll();
213215
}
214216
}
217+
218+
return succeeded;
215219
} else if (form_result.type === 'redirect') {
216220
const stringified_refreshes = form_result.refreshes ?? '';
217221
if (stringified_refreshes) {
218222
apply_refreshes(stringified_refreshes);
219223
}
220224
// Use internal version to allow redirects to external URLs
221225
void _goto(form_result.location, { invalidateAll: !stringified_refreshes }, 0);
226+
return true;
222227
} else {
223228
throw new HttpError(form_result.status ?? 500, form_result.error);
224229
}
@@ -434,8 +439,8 @@ export function form(id) {
434439

435440
instance[createAttachmentKey()] = create_attachment(
436441
form_onsubmit(({ submit, form }) =>
437-
submit().then(() => {
438-
if (!issues.$) {
442+
submit().then((succeeded) => {
443+
if (succeeded) {
439444
form.reset();
440445
}
441446
})

packages/kit/src/runtime/server/page/render.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -608,8 +608,14 @@ export async function render_response({
608608
// we use an anonymous function instead of an arrow function to support
609609
// older browsers (https://github.com/sveltejs/kit/pull/5417)
610610
blocks.push(`if ('serviceWorker' in navigator) {
611+
const script_url = '${prefixed('service-worker.js')}';
612+
const policy = globalThis?.window?.trustedTypes?.createPolicy(
613+
'sveltekit-trusted-url',
614+
{ createScriptURL(url) { return url; } }
615+
);
616+
const sanitised = policy?.createScriptURL(script_url) ?? script_url;
611617
addEventListener('load', function () {
612-
navigator.serviceWorker.register('${prefixed('service-worker.js')}'${opts});
618+
navigator.serviceWorker.register(sanitised${opts});
613619
});
614620
}`);
615621
}

packages/kit/test/apps/async/src/routes/remote/form/[test_name]/+page.svelte

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
88
const scoped = set_message.for(`scoped:${params.test_name}`);
99
const enhanced = set_message.for(`enhanced:${params.test_name}`);
10+
11+
let submit_result = $state('none');
1012
</script>
1113

1214
<p>message.current: {message.current}</p>
@@ -52,7 +54,9 @@
5254
<form
5355
data-enhanced
5456
{...enhanced.enhance(async ({ data, submit }) => {
55-
await submit().updates(message.withOverride(() => data.message + ' (override)'));
57+
submit_result = String(
58+
await submit().updates(message.withOverride(() => data.message + ' (override)'))
59+
);
5660
})}
5761
>
5862
{#if enhanced.fields.message.issues()}
@@ -67,6 +71,7 @@
6771
<p>enhanced.input.message: {enhanced.fields.message.value()}</p>
6872
<p>enhanced.pending: {enhanced.pending}</p>
6973
<p>enhanced.result: {enhanced.result}</p>
74+
<p>enhanced.submit_result: {submit_result}</p>
7075

7176
<hr />
7277

0 commit comments

Comments
 (0)