@sveltejs/kit 3.0.0-next.11 → 3.0.0-next.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli.js +8 -8
- package/src/constants.js +1 -1
- package/src/core/config/options.js +43 -35
- package/src/core/env.js +25 -11
- package/src/core/sync/sync.js +11 -14
- package/src/core/sync/utils.js +21 -1
- package/src/core/sync/{write_non_ambient.js → write_app_types.js} +23 -21
- package/src/core/sync/write_client_manifest.js +4 -12
- package/src/core/sync/write_env.js +6 -4
- package/src/core/sync/write_server.js +10 -15
- package/src/core/sync/write_tsconfig/index.js +245 -0
- package/src/core/sync/write_tsconfig/utils.js +162 -0
- package/src/core/sync/write_types/index.js +80 -88
- package/src/exports/internal/server/index.js +3 -1
- package/src/exports/public.d.ts +28 -51
- package/src/exports/vite/dev/index.js +88 -76
- package/src/exports/vite/index.js +364 -144
- package/src/exports/vite/module_ids.js +3 -1
- package/src/exports/vite/utils.js +1 -10
- package/src/runner.js +13 -0
- package/src/runtime/app/forms.js +4 -0
- package/src/runtime/app/manifest/index.js +1 -0
- package/src/runtime/app/paths/client.js +30 -30
- package/src/runtime/app/paths/internal/client.js +26 -0
- package/src/runtime/app/paths/server.js +22 -9
- package/src/runtime/app/paths/types.d.ts +11 -19
- package/src/runtime/app/server/remote/command.js +12 -7
- package/src/runtime/app/server/remote/form.js +29 -26
- package/src/runtime/app/server/remote/prerender.js +9 -2
- package/src/runtime/app/server/remote/query.js +5 -4
- package/src/runtime/app/server/remote/shared.js +48 -30
- package/src/runtime/app/service-worker/index.js +24 -0
- package/src/runtime/app/state/index.js +1 -1
- package/src/runtime/client/client.js +46 -9
- package/src/runtime/client/remote-functions/form.svelte.js +43 -51
- package/src/runtime/client/remote-functions/query/index.js +2 -2
- package/src/runtime/client/remote-functions/query-batch.svelte.js +2 -3
- package/src/runtime/client/remote-functions/query-live/iterator.js +5 -2
- package/src/runtime/client/remote-functions/shared.svelte.js +4 -1
- package/src/runtime/client/state.svelte.js +54 -21
- package/src/runtime/form-utils.js +89 -54
- package/src/runtime/server/cookie.js +1 -1
- package/src/runtime/server/data/index.js +31 -28
- package/src/runtime/server/errors.js +1 -1
- package/src/runtime/server/page/actions.js +3 -3
- package/src/runtime/server/page/render.js +19 -28
- package/src/runtime/server/remote-functions.js +66 -35
- package/src/runtime/server/respond.js +56 -14
- package/src/runtime/server/utils.js +10 -0
- package/src/types/ambient-private.d.ts +8 -0
- package/src/types/ambient.d.ts +22 -27
- package/src/types/global-private.d.ts +12 -0
- package/src/types/internal.d.ts +13 -3
- package/src/utils/url.js +12 -0
- package/src/version.js +1 -1
- package/types/index.d.ts +80 -98
- package/types/index.d.ts.map +4 -1
- package/src/core/sync/write_ambient.js +0 -18
- package/src/core/sync/write_tsconfig.js +0 -258
- /package/src/core/sync/{write_tsconfig_test → write_tsconfig/test-app}/package.json +0 -0
|
@@ -10,21 +10,58 @@ import { SvelteKitError } from '@sveltejs/kit/internal';
|
|
|
10
10
|
const decoder = new TextDecoder();
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Sets a value in a nested object
|
|
13
|
+
* Sets a parsed form field value in a nested object, mutating the original object.
|
|
14
14
|
* @param {Record<string, any>} object
|
|
15
|
-
* @param {string}
|
|
15
|
+
* @param {{ name: string; type: 'number' | 'boolean' | null }} field
|
|
16
16
|
* @param {any} value
|
|
17
17
|
*/
|
|
18
|
-
export function set_nested_value(object,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
export function set_nested_value(object, field, value) {
|
|
19
|
+
deep_set(object, split_path(field.name), value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Separates a form field's path from the metadata encoded in its name.
|
|
24
|
+
* @param {string} form_id
|
|
25
|
+
* @param {string} key
|
|
26
|
+
* @returns {{ name: string; type: 'number' | 'boolean' | null; is_array: boolean }}
|
|
27
|
+
*/
|
|
28
|
+
export function parse_form_key(form_id, key) {
|
|
29
|
+
const suffix = '/' + form_id;
|
|
30
|
+
let name = key;
|
|
31
|
+
|
|
32
|
+
if (!name.endsWith(suffix)) {
|
|
33
|
+
throw new Error(`Form contained a field that wasn't created with form.fields.as(...): ${name}`);
|
|
25
34
|
}
|
|
26
35
|
|
|
27
|
-
|
|
36
|
+
name = name.slice(0, -suffix.length);
|
|
37
|
+
|
|
38
|
+
/** @type {'number' | 'boolean' | null} */
|
|
39
|
+
let type = null;
|
|
40
|
+
|
|
41
|
+
if (name.startsWith('n:')) {
|
|
42
|
+
name = name.slice(2);
|
|
43
|
+
type = 'number';
|
|
44
|
+
} else if (name.startsWith('b:')) {
|
|
45
|
+
name = name.slice(2);
|
|
46
|
+
type = 'boolean';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const is_array = name.endsWith('[]');
|
|
50
|
+
if (is_array) name = name.slice(0, -2);
|
|
51
|
+
|
|
52
|
+
return { name, type, is_array };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {'number' | 'boolean' | null} type
|
|
57
|
+
* @param {any} value
|
|
58
|
+
* @returns {any}
|
|
59
|
+
*/
|
|
60
|
+
export function coerce_form_value(type, value) {
|
|
61
|
+
if (Array.isArray(value)) return value.map((value) => coerce_form_value(type, value));
|
|
62
|
+
if (type === 'number') return value === '' ? undefined : parseFloat(value);
|
|
63
|
+
if (type === 'boolean') return value === 'on';
|
|
64
|
+
return value;
|
|
28
65
|
}
|
|
29
66
|
|
|
30
67
|
/** Pass this to set_nested_value to delete the last part of the given path */
|
|
@@ -32,38 +69,36 @@ export const DELETE_KEY = {};
|
|
|
32
69
|
|
|
33
70
|
/**
|
|
34
71
|
* Convert `FormData` into a POJO
|
|
72
|
+
* @param {string} form_id
|
|
35
73
|
* @param {FormData} data
|
|
36
74
|
*/
|
|
37
|
-
export function convert_formdata(data) {
|
|
75
|
+
export function convert_formdata(form_id, data) {
|
|
38
76
|
/** @type {Record<string, any>} */
|
|
39
77
|
const result = {};
|
|
40
78
|
|
|
41
|
-
for (
|
|
42
|
-
const is_array = key.endsWith('[]');
|
|
79
|
+
for (const field_name of data.keys()) {
|
|
43
80
|
/** @type {any[]} */
|
|
44
|
-
|
|
81
|
+
const values = data.getAll(field_name);
|
|
45
82
|
|
|
46
|
-
|
|
83
|
+
const field = parse_form_key(form_id, field_name);
|
|
47
84
|
|
|
48
85
|
// an empty `<input type="file">` will submit a non-existent file, bizarrely
|
|
49
|
-
|
|
86
|
+
const entries = values.filter(
|
|
50
87
|
(entry) => typeof entry === 'string' || entry.name !== '' || entry.size > 0
|
|
51
88
|
);
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
if (key.startsWith('n:')) {
|
|
55
|
-
key = key.slice(2);
|
|
56
|
-
values = values.map((v) => (v === '' ? undefined : parseFloat(/** @type {string} */ (v))));
|
|
57
|
-
} else if (key.startsWith('b:')) {
|
|
58
|
-
key = key.slice(2);
|
|
59
|
-
values = values.map((v) => v === 'on');
|
|
60
|
-
}
|
|
89
|
+
if (entries.length === 0 && !field.is_array) continue;
|
|
61
90
|
|
|
62
|
-
if (
|
|
63
|
-
throw new Error(
|
|
91
|
+
if (entries.length > 1 && !field.is_array) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`Form cannot contain duplicated keys — "${field.name}" has ${entries.length} values`
|
|
94
|
+
);
|
|
64
95
|
}
|
|
65
96
|
|
|
66
|
-
set_nested_value(
|
|
97
|
+
set_nested_value(
|
|
98
|
+
result,
|
|
99
|
+
field,
|
|
100
|
+
coerce_form_value(field.type, field.is_array ? entries : entries[0])
|
|
101
|
+
);
|
|
67
102
|
}
|
|
68
103
|
|
|
69
104
|
return result;
|
|
@@ -139,12 +174,13 @@ export function serialize_binary_form(data, meta) {
|
|
|
139
174
|
|
|
140
175
|
/**
|
|
141
176
|
* @param {Request} request
|
|
177
|
+
* @param {string} form_id
|
|
142
178
|
* @returns {Promise<{ data: Record<string, any>; meta: BinaryFormMeta; form_data: FormData | null }>}
|
|
143
179
|
*/
|
|
144
|
-
export async function deserialize_binary_form(request) {
|
|
180
|
+
export async function deserialize_binary_form(request, form_id) {
|
|
145
181
|
if (request.headers.get('content-type') !== BINARY_FORM_CONTENT_TYPE) {
|
|
146
182
|
const form_data = await request.formData();
|
|
147
|
-
return { data: convert_formdata(form_data), meta: {}, form_data };
|
|
183
|
+
return { data: convert_formdata(form_id, form_data), meta: {}, form_data };
|
|
148
184
|
}
|
|
149
185
|
if (!request.body) {
|
|
150
186
|
throw deserialize_error('no body');
|
|
@@ -653,18 +689,21 @@ function deep_clone(value) {
|
|
|
653
689
|
|
|
654
690
|
/**
|
|
655
691
|
* Creates a proxy-based field accessor for form data
|
|
692
|
+
* @param {{
|
|
693
|
+
* form_id: string,
|
|
694
|
+
* get: () => Record<string, any>,
|
|
695
|
+
* set: (path: (string | number)[], value: any) => void,
|
|
696
|
+
* get_issues: (path?: (string | number)[], all?: boolean) => Record<string, InternalRemoteFormIssue[]>,
|
|
697
|
+
* get_touched: () => Record<string, boolean>,
|
|
698
|
+
* get_dirty: () => Record<string, boolean>
|
|
699
|
+
* }} context - Form context, including value accessors and form metadata
|
|
656
700
|
* @param {any} target - Function or empty POJO
|
|
657
|
-
* @param {() => Record<string, any>} get - Function to get current input data
|
|
658
|
-
* @param {(path: (string | number)[], value: any) => void} set - Function to set input data
|
|
659
|
-
* @param {(path?: (string | number)[], all?: boolean) => Record<string, InternalRemoteFormIssue[]>} get_issues - Function to get current issues
|
|
660
|
-
* @param {() => Record<string, boolean>} get_touched - Function to get touched fields
|
|
661
|
-
* @param {() => Record<string, boolean>} get_dirty - Function to get dirty fields
|
|
662
701
|
* @param {(string | number)[]} path - Current access path
|
|
663
702
|
* @returns {any} Proxy object with name(), value(), and issues() methods
|
|
664
703
|
*/
|
|
665
|
-
export function create_field_proxy(
|
|
704
|
+
export function create_field_proxy(context, target = {}, path = []) {
|
|
666
705
|
const get_value = () => {
|
|
667
|
-
const value = deep_get(get(), path);
|
|
706
|
+
const value = deep_get(context.get(), path);
|
|
668
707
|
return deep_clone(value);
|
|
669
708
|
};
|
|
670
709
|
|
|
@@ -674,10 +713,7 @@ export function create_field_proxy(target, get, set, get_issues, get_touched, ge
|
|
|
674
713
|
|
|
675
714
|
// Handle array access like jobs[0]
|
|
676
715
|
if (/^\d+$/.test(prop)) {
|
|
677
|
-
return create_field_proxy({},
|
|
678
|
-
...path,
|
|
679
|
-
parseInt(prop, 10)
|
|
680
|
-
]);
|
|
716
|
+
return create_field_proxy(context, {}, [...path, parseInt(prop, 10)]);
|
|
681
717
|
}
|
|
682
718
|
|
|
683
719
|
const key = build_path_string(path);
|
|
@@ -685,20 +721,19 @@ export function create_field_proxy(target, get, set, get_issues, get_touched, ge
|
|
|
685
721
|
|
|
686
722
|
if (prop === 'set') {
|
|
687
723
|
const set_func = function (/** @type {any} */ newValue) {
|
|
688
|
-
set(path, newValue);
|
|
724
|
+
context.set(path, newValue);
|
|
689
725
|
return newValue;
|
|
690
726
|
};
|
|
691
|
-
|
|
692
|
-
return create_field_proxy(set_func, get, set, get_issues, get_touched, get_dirty, next);
|
|
727
|
+
return create_field_proxy(context, set_func, next);
|
|
693
728
|
}
|
|
694
729
|
|
|
695
730
|
if (prop === 'value') {
|
|
696
|
-
return create_field_proxy(
|
|
731
|
+
return create_field_proxy(context, get_value, next);
|
|
697
732
|
}
|
|
698
733
|
|
|
699
734
|
if (prop === 'issues' || prop === 'allIssues') {
|
|
700
735
|
const issues_func = () => {
|
|
701
|
-
const all_issues = get_issues(path, prop === 'allIssues')[key === '' ? '$' : key];
|
|
736
|
+
const all_issues = context.get_issues(path, prop === 'allIssues')[key === '' ? '$' : key];
|
|
702
737
|
|
|
703
738
|
if (prop === 'allIssues') {
|
|
704
739
|
return all_issues?.map((issue) => ({
|
|
@@ -717,12 +752,12 @@ export function create_field_proxy(target, get, set, get_issues, get_touched, ge
|
|
|
717
752
|
return issues?.length ? issues : undefined;
|
|
718
753
|
};
|
|
719
754
|
|
|
720
|
-
return create_field_proxy(
|
|
755
|
+
return create_field_proxy(context, issues_func, next);
|
|
721
756
|
}
|
|
722
757
|
|
|
723
758
|
if (prop === 'touched' || prop === 'dirty') {
|
|
724
759
|
const fn = () => {
|
|
725
|
-
const object = prop === 'dirty' ? get_dirty() : get_touched();
|
|
760
|
+
const object = prop === 'dirty' ? context.get_dirty() : context.get_touched();
|
|
726
761
|
|
|
727
762
|
if (key === '') {
|
|
728
763
|
return Object.keys(object).length > 0;
|
|
@@ -745,7 +780,7 @@ export function create_field_proxy(target, get, set, get_issues, get_touched, ge
|
|
|
745
780
|
return false;
|
|
746
781
|
};
|
|
747
782
|
|
|
748
|
-
return create_field_proxy(
|
|
783
|
+
return create_field_proxy(context, fn, next);
|
|
749
784
|
}
|
|
750
785
|
|
|
751
786
|
if (prop === 'as') {
|
|
@@ -759,14 +794,14 @@ export function create_field_proxy(target, get, set, get_issues, get_touched, ge
|
|
|
759
794
|
type === 'select multiple' ||
|
|
760
795
|
(type === 'checkbox' && typeof input_value === 'string');
|
|
761
796
|
|
|
762
|
-
const
|
|
797
|
+
const type_prefix = get_type_prefix(type, is_array, input_value);
|
|
763
798
|
|
|
764
799
|
// Base properties for all input types
|
|
765
800
|
/** @type {Record<string, any>} */
|
|
766
801
|
const base_props = {
|
|
767
|
-
name:
|
|
802
|
+
name: type_prefix + key + (is_array ? '[]' : '') + '/' + context.form_id,
|
|
768
803
|
get 'aria-invalid'() {
|
|
769
|
-
const issues = get_issues();
|
|
804
|
+
const issues = context.get_issues();
|
|
770
805
|
return key in issues ? 'true' : undefined;
|
|
771
806
|
}
|
|
772
807
|
};
|
|
@@ -911,11 +946,11 @@ export function create_field_proxy(target, get, set, get_issues, get_touched, ge
|
|
|
911
946
|
});
|
|
912
947
|
};
|
|
913
948
|
|
|
914
|
-
return create_field_proxy(
|
|
949
|
+
return create_field_proxy(context, as_func, next);
|
|
915
950
|
}
|
|
916
951
|
|
|
917
952
|
// Handle property access (nested fields)
|
|
918
|
-
return create_field_proxy({},
|
|
953
|
+
return create_field_proxy(context, {}, next);
|
|
919
954
|
}
|
|
920
955
|
});
|
|
921
956
|
}
|
|
@@ -73,7 +73,7 @@ export function get_cookies(request, url) {
|
|
|
73
73
|
httpOnly: true,
|
|
74
74
|
path: '/',
|
|
75
75
|
sameSite: 'lax',
|
|
76
|
-
secure: url.hostname === 'localhost' && url.protocol === 'http:'
|
|
76
|
+
secure: !__SVELTEKIT_DEV__ && !(url.hostname === 'localhost' && url.protocol === 'http:')
|
|
77
77
|
};
|
|
78
78
|
|
|
79
79
|
/** @type {import('@sveltejs/kit').Cookies} */
|
|
@@ -7,6 +7,7 @@ import { load_server_data } from '../page/load_data.js';
|
|
|
7
7
|
import { handle_error_and_jsonify } from '../errors.js';
|
|
8
8
|
import { normalize_path } from '../../../utils/url.js';
|
|
9
9
|
import { text_encoder } from '../../utils.js';
|
|
10
|
+
import { with_version_header } from '../utils.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* @param {import('@sveltejs/kit').RequestEvent} event
|
|
@@ -31,9 +32,7 @@ export async function render_data(
|
|
|
31
32
|
) {
|
|
32
33
|
if (!route.page) {
|
|
33
34
|
// requesting /__data.json should fail for a +server.js
|
|
34
|
-
return new Response(undefined, {
|
|
35
|
-
status: 404
|
|
36
|
-
});
|
|
35
|
+
return with_version_header(new Response(undefined, { status: 404 }));
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
try {
|
|
@@ -123,26 +122,28 @@ export async function render_data(
|
|
|
123
122
|
return json_response(data);
|
|
124
123
|
}
|
|
125
124
|
|
|
126
|
-
return
|
|
127
|
-
new
|
|
128
|
-
|
|
129
|
-
controller
|
|
130
|
-
|
|
131
|
-
|
|
125
|
+
return with_version_header(
|
|
126
|
+
new Response(
|
|
127
|
+
new ReadableStream({
|
|
128
|
+
async start(controller) {
|
|
129
|
+
controller.enqueue(text_encoder.encode(data));
|
|
130
|
+
for await (const chunk of chunks) {
|
|
131
|
+
controller.enqueue(text_encoder.encode(chunk));
|
|
132
|
+
}
|
|
133
|
+
controller.close();
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
type: 'bytes'
|
|
137
|
+
}),
|
|
138
|
+
{
|
|
139
|
+
headers: {
|
|
140
|
+
// we use a proprietary content type to prevent buffering.
|
|
141
|
+
// the `text` prefix makes it inspectable
|
|
142
|
+
'content-type': 'text/sveltekit-data',
|
|
143
|
+
'cache-control': 'private, no-store'
|
|
132
144
|
}
|
|
133
|
-
controller.close();
|
|
134
|
-
},
|
|
135
|
-
|
|
136
|
-
type: 'bytes'
|
|
137
|
-
}),
|
|
138
|
-
{
|
|
139
|
-
headers: {
|
|
140
|
-
// we use a proprietary content type to prevent buffering.
|
|
141
|
-
// the `text` prefix makes it inspectable
|
|
142
|
-
'content-type': 'text/sveltekit-data',
|
|
143
|
-
'cache-control': 'private, no-store'
|
|
144
145
|
}
|
|
145
|
-
|
|
146
|
+
)
|
|
146
147
|
);
|
|
147
148
|
} catch (e) {
|
|
148
149
|
const error = normalize_error(e);
|
|
@@ -161,13 +162,15 @@ export async function render_data(
|
|
|
161
162
|
* @param {number} [status]
|
|
162
163
|
*/
|
|
163
164
|
function json_response(json, status = 200) {
|
|
164
|
-
return
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
165
|
+
return with_version_header(
|
|
166
|
+
text(typeof json === 'string' ? json : JSON.stringify(json), {
|
|
167
|
+
status,
|
|
168
|
+
headers: {
|
|
169
|
+
'content-type': 'application/json',
|
|
170
|
+
'cache-control': 'private, no-store'
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
);
|
|
171
174
|
}
|
|
172
175
|
|
|
173
176
|
/**
|
|
@@ -17,7 +17,7 @@ export async function handle_fatal_error(event, state, options, error) {
|
|
|
17
17
|
const body = await handle_error_and_jsonify(event, state, options, error);
|
|
18
18
|
const status = body.status;
|
|
19
19
|
|
|
20
|
-
//
|
|
20
|
+
// sec-fetch-dest would be nicer, but non-browser clients and plain HTTP hosts don't send it
|
|
21
21
|
const type = negotiate(event.request.headers.get('accept') || 'text/html', [
|
|
22
22
|
'application/json',
|
|
23
23
|
'text/html'
|
|
@@ -7,7 +7,7 @@ import { HttpError, Redirect, ActionFailure, SvelteKitError } from '@sveltejs/ki
|
|
|
7
7
|
import { with_request_store, merge_tracing } from '@sveltejs/kit/internal/server';
|
|
8
8
|
import { normalize_error } from '../../../utils/error.js';
|
|
9
9
|
import { is_form_content_type, negotiate } from '../../../utils/http.js';
|
|
10
|
-
import { create_replacer } from '../utils.js';
|
|
10
|
+
import { create_replacer, with_version_header } from '../utils.js';
|
|
11
11
|
import { handle_error_and_jsonify } from '../errors.js';
|
|
12
12
|
import { record_span } from '../../telemetry/record_span.js';
|
|
13
13
|
|
|
@@ -95,7 +95,7 @@ export async function handle_action_json_request(event, event_state, options, se
|
|
|
95
95
|
});
|
|
96
96
|
} else {
|
|
97
97
|
// no data returned — use 204 No Content (without a body, per the spec)
|
|
98
|
-
return new Response(null, { status: 204 });
|
|
98
|
+
return with_version_header(new Response(null, { status: 204 }));
|
|
99
99
|
}
|
|
100
100
|
} catch (e) {
|
|
101
101
|
const err = normalize_error(e);
|
|
@@ -148,7 +148,7 @@ export function action_json_redirect(redirect) {
|
|
|
148
148
|
* @param {ResponseInit} [init]
|
|
149
149
|
*/
|
|
150
150
|
function action_json(data, init) {
|
|
151
|
-
return json(data, init);
|
|
151
|
+
return with_version_header(json(data, init));
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
/**
|
|
@@ -304,6 +304,19 @@ export async function render_response({
|
|
|
304
304
|
head.add_style(style, attributes);
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
+
/**
|
|
308
|
+
* see the `output.linkHeaderPreload` option for details on why we have multiple options here
|
|
309
|
+
* @param {string} path
|
|
310
|
+
* @param {string[]} attributes
|
|
311
|
+
*/
|
|
312
|
+
const add_preload = (path, attributes) => {
|
|
313
|
+
if (options.link_header_preload && !state.prerendering) {
|
|
314
|
+
link_headers.add(`<${encodeURI(path)}>; ${attributes.join('; ')}; nopush`);
|
|
315
|
+
} else {
|
|
316
|
+
head.add_link_tag(path, attributes);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
307
320
|
for (const dep of stylesheets) {
|
|
308
321
|
const path = prefixed(dep);
|
|
309
322
|
|
|
@@ -328,18 +341,7 @@ export async function render_response({
|
|
|
328
341
|
if (resolve_opts.preload({ type: 'font', path })) {
|
|
329
342
|
const ext = dep.slice(dep.lastIndexOf('.') + 1);
|
|
330
343
|
|
|
331
|
-
|
|
332
|
-
link_headers.add(
|
|
333
|
-
`<${encodeURI(path)}>; rel="preload"; as="font"; type="font/${ext}"; crossorigin; nopush`
|
|
334
|
-
);
|
|
335
|
-
} else {
|
|
336
|
-
head.add_link_tag(path, [
|
|
337
|
-
'rel="preload"',
|
|
338
|
-
'as="font"',
|
|
339
|
-
`type="font/${ext}"`,
|
|
340
|
-
'crossorigin'
|
|
341
|
-
]);
|
|
342
|
-
}
|
|
344
|
+
add_preload(path, ['rel="preload"', 'as="font"', `type="font/${ext}"`, 'crossorigin']);
|
|
343
345
|
}
|
|
344
346
|
}
|
|
345
347
|
|
|
@@ -368,23 +370,12 @@ export async function render_response({
|
|
|
368
370
|
}
|
|
369
371
|
|
|
370
372
|
if (!client.inline) {
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
);
|
|
374
|
-
|
|
375
|
-
/** @type {(path: string) => void} */
|
|
376
|
-
let add_preload;
|
|
373
|
+
for (const dep of modulepreloads) {
|
|
374
|
+
const path = prefixed(dep);
|
|
377
375
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
link_headers.add(`<${encodeURI(path)}>; rel="modulepreload"; nopush`);
|
|
382
|
-
} else {
|
|
383
|
-
add_preload = (path) => head.add_link_tag(path, ['rel="modulepreload"']);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
for (const path of included_modulepreloads) {
|
|
387
|
-
add_preload(path);
|
|
376
|
+
if (resolve_opts.preload({ type: 'js', path })) {
|
|
377
|
+
add_preload(path, ['rel="modulepreload"']);
|
|
378
|
+
}
|
|
388
379
|
}
|
|
389
380
|
}
|
|
390
381
|
|
|
@@ -13,6 +13,7 @@ import { check_incorrect_fail_use } from './page/actions.js';
|
|
|
13
13
|
import { DEV } from 'esm-env';
|
|
14
14
|
import { record_span } from '../telemetry/record_span.js';
|
|
15
15
|
import { deserialize_binary_form } from '../form-utils.js';
|
|
16
|
+
import { with_version_header } from './utils.js';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* How long (in milliseconds) to wait after the last message was sent before
|
|
@@ -28,11 +29,12 @@ export async function handle_remote_call(event, state, options, manifest, id) {
|
|
|
28
29
|
attributes: {
|
|
29
30
|
'sveltekit.remote.call.id': id
|
|
30
31
|
},
|
|
31
|
-
fn: (current) => {
|
|
32
|
+
fn: async (current) => {
|
|
32
33
|
const traced_event = merge_tracing(event, current);
|
|
33
|
-
|
|
34
|
+
const response = await with_request_store({ event: traced_event, state }, () =>
|
|
34
35
|
handle_remote_call_internal(traced_event, state, options, manifest, id)
|
|
35
36
|
);
|
|
37
|
+
return with_version_header(response);
|
|
36
38
|
}
|
|
37
39
|
});
|
|
38
40
|
}
|
|
@@ -240,7 +242,11 @@ async function handle_remote_call_internal(event, state, options, manifest, id)
|
|
|
240
242
|
);
|
|
241
243
|
}
|
|
242
244
|
|
|
243
|
-
const {
|
|
245
|
+
const {
|
|
246
|
+
data: input,
|
|
247
|
+
meta,
|
|
248
|
+
form_data
|
|
249
|
+
} = await deserialize_binary_form(event.request, internals.id);
|
|
244
250
|
state.remote.requested = create_requested_map(meta.remote_refreshes);
|
|
245
251
|
|
|
246
252
|
// If this is a keyed form instance (created via form.for(key)), add the key to the form data (unless already set)
|
|
@@ -368,43 +374,64 @@ export async function collect_remote_data(data, event, state, options) {
|
|
|
368
374
|
/** @type {Promise<any>[]} */
|
|
369
375
|
const promises = [];
|
|
370
376
|
|
|
377
|
+
// Keys the explicit pass has serialized. Invoking a query's `fn` there can, as a
|
|
378
|
+
// side effect, register the same query in `state.remote.implicit` (via
|
|
379
|
+
// `get_response`), so we skip those keys in the implicit pass below to avoid
|
|
380
|
+
// processing them twice.
|
|
381
|
+
/** @type {Set<string>} */
|
|
382
|
+
const processed = new Set();
|
|
383
|
+
|
|
371
384
|
if (state.remote.explicit) {
|
|
372
|
-
|
|
373
|
-
// there were explicit refreshes/reconnects (via `refresh()`/`set()`/`reconnect()`),
|
|
374
|
-
// so the client should apply these single-flight updates instead of calling `invalidateAll()`
|
|
375
|
-
data.r = true;
|
|
385
|
+
const { explicit } = state.remote;
|
|
376
386
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
387
|
+
/** @type {Promise<void>[]} */
|
|
388
|
+
const inflight = [];
|
|
389
|
+
|
|
390
|
+
const drain = () => {
|
|
391
|
+
for (const [remote_key, { internals, fn }] of explicit) {
|
|
392
|
+
explicit.delete(remote_key);
|
|
393
|
+
if (processed.has(remote_key)) continue;
|
|
394
|
+
processed.add(remote_key);
|
|
395
|
+
|
|
396
|
+
// there were explicit refreshes/reconnects (via `refresh()`/`set()`/`reconnect()`),
|
|
397
|
+
// so the client should apply these single-flight updates instead of calling `invalidateAll()`
|
|
398
|
+
data.r = true;
|
|
380
399
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
400
|
+
const type = /** @type {'p' | 'q' | 'l'} */ (
|
|
401
|
+
internals.type === 'query_live' ? 'l' : internals.type[0]
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
// `fn` is deferred until now so the query runs after any state mutations
|
|
405
|
+
// in the command/form body. If the query was re-awaited in the meantime,
|
|
406
|
+
// `fn` returns the existing (fresh) cache entry rather than re-running.
|
|
407
|
+
inflight.push(
|
|
408
|
+
fn().then(
|
|
409
|
+
(v) => {
|
|
410
|
+
// a fresh value replaces the node entirely, so a re-run can't leave
|
|
411
|
+
// a stale error from a previous run alongside the new value
|
|
412
|
+
(data[type] ??= {})[remote_key] = { v };
|
|
413
|
+
drain();
|
|
414
|
+
},
|
|
415
|
+
async (e) => {
|
|
416
|
+
if (!(e instanceof Redirect)) {
|
|
417
|
+
// (a Redirect is already handled elsewhere)
|
|
418
|
+
(data[type] ??= {})[remote_key] = { e: await convert_error(e) };
|
|
419
|
+
}
|
|
420
|
+
drain();
|
|
397
421
|
}
|
|
422
|
+
)
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
};
|
|
398
426
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
427
|
+
drain();
|
|
428
|
+
|
|
429
|
+
// `inflight` grows as settles drain newly-refreshed queries
|
|
430
|
+
for (const promise of inflight) {
|
|
431
|
+
await promise;
|
|
403
432
|
}
|
|
404
433
|
}
|
|
405
434
|
|
|
406
|
-
await Promise.all(promises);
|
|
407
|
-
|
|
408
435
|
if (state.remote.implicit) {
|
|
409
436
|
for (const [internals, record] of state.remote.implicit) {
|
|
410
437
|
// Private (non-exported) remote functions have no `id` and must never be
|
|
@@ -416,6 +443,10 @@ export async function collect_remote_data(data, event, state, options) {
|
|
|
416
443
|
// form outputs are registered under the client-side action id directly
|
|
417
444
|
const remote_key = internals.type === 'form' ? key : create_remote_key(internals.id, key);
|
|
418
445
|
|
|
446
|
+
// already serialized by the explicit pass (which always awaits and wins),
|
|
447
|
+
// so don't reprocess it here with the implicit "still loading" heuristic
|
|
448
|
+
if (processed.has(remote_key)) continue;
|
|
449
|
+
|
|
419
450
|
const type = /** @type {'p' | 'q' | 'l' | 'f'} */ (
|
|
420
451
|
internals.type === 'query_live' ? 'l' : internals.type[0]
|
|
421
452
|
);
|
|
@@ -540,9 +571,9 @@ async function handle_remote_form_post_internal(event, state, manifest, id) {
|
|
|
540
571
|
}
|
|
541
572
|
|
|
542
573
|
try {
|
|
543
|
-
const
|
|
574
|
+
const __ = /** @type {RemoteFormInternals} */ (/** @type {any} */ (form).__);
|
|
544
575
|
|
|
545
|
-
const { data, meta, form_data } = await deserialize_binary_form(event.request);
|
|
576
|
+
const { data, meta, form_data } = await deserialize_binary_form(event.request, __.id);
|
|
546
577
|
|
|
547
578
|
if (action_id && !('id' in data)) {
|
|
548
579
|
data.id = JSON.parse(decodeURIComponent(action_id));
|
|
@@ -550,7 +581,7 @@ async function handle_remote_form_post_internal(event, state, manifest, id) {
|
|
|
550
581
|
|
|
551
582
|
await with_request_store(
|
|
552
583
|
{ event, state: { ...state, is_in_remote_form_or_command: true } },
|
|
553
|
-
() => fn(data, meta, form_data)
|
|
584
|
+
() => __.fn(data, meta, form_data)
|
|
554
585
|
);
|
|
555
586
|
|
|
556
587
|
// We don't want the data to appear on `let { form } = $props()`, which is why we're not returning it.
|