@sveltejs/kit 1.7.1 → 1.8.0

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.
@@ -25,8 +25,6 @@ import {
25
25
  } from './fetcher.js';
26
26
  import { parse } from './parse.js';
27
27
 
28
- import Root from '__GENERATED__/root.svelte';
29
- import { nodes, server_loads, dictionary, matchers, hooks } from '__CLIENT__/manifest.js';
30
28
  import { base } from '__sveltekit/paths';
31
29
  import { HttpError, Redirect } from '../control.js';
32
30
  import { stores } from './singletons.js';
@@ -36,16 +34,6 @@ import { INDEX_KEY, PRELOAD_PRIORITIES, SCROLL_KEY, SNAPSHOT_KEY } from './const
36
34
  import { validate_common_exports } from '../../utils/exports.js';
37
35
  import { compact } from '../../utils/array.js';
38
36
 
39
- const routes = parse(nodes, server_loads, dictionary, matchers);
40
-
41
- const default_layout_loader = nodes[0];
42
- const default_error_loader = nodes[1];
43
-
44
- // we import the root layout/error nodes eagerly, so that
45
- // connectivity errors after initialisation don't nuke the app
46
- default_layout_loader();
47
- default_error_loader();
48
-
49
37
  // We track the scroll position associated with each history entry in sessionStorage,
50
38
  // rather than on history.state itself, because when navigation is driven by
51
39
  // popstate it's too late to update the scroll position associated with the
@@ -64,12 +52,21 @@ function update_scroll_positions(index) {
64
52
  }
65
53
 
66
54
  /**
67
- * @param {{
68
- * target: HTMLElement;
69
- * }} opts
55
+ * @param {import('./types').SvelteKitApp} app
56
+ * @param {HTMLElement} target
70
57
  * @returns {import('./types').Client}
71
58
  */
72
- export function create_client({ target }) {
59
+ export function create_client(app, target) {
60
+ const routes = parse(app);
61
+
62
+ const default_layout_loader = app.nodes[0];
63
+ const default_error_loader = app.nodes[1];
64
+
65
+ // we import the root layout/error nodes eagerly, so that
66
+ // connectivity errors after initialisation don't nuke the app
67
+ default_layout_loader();
68
+ default_error_loader();
69
+
73
70
  const container = __SVELTEKIT_EMBEDDED__ ? target : document.documentElement;
74
71
  /** @type {Array<((url: URL) => boolean)>} */
75
72
  const invalidated = [];
@@ -432,7 +429,7 @@ export function create_client({ target }) {
432
429
 
433
430
  page = /** @type {import('types').Page} */ (result.props.page);
434
431
 
435
- root = new Root({
432
+ root = new app.root({
436
433
  target,
437
434
  props: { ...result.props, stores, components },
438
435
  hydrate: true
@@ -736,22 +733,8 @@ export function create_client({ target }) {
736
733
  * @returns {import('./types').DataNode | null}
737
734
  */
738
735
  function create_data_node(node, previous) {
739
- if (node?.type === 'data') {
740
- return {
741
- type: 'data',
742
- data: node.data,
743
- uses: {
744
- dependencies: new Set(node.uses.dependencies ?? []),
745
- params: new Set(node.uses.params ?? []),
746
- parent: !!node.uses.parent,
747
- route: !!node.uses.route,
748
- url: !!node.uses.url
749
- },
750
- slash: node.slash
751
- };
752
- } else if (node?.type === 'skip') {
753
- return previous ?? null;
754
- }
736
+ if (node?.type === 'data') return node;
737
+ if (node?.type === 'skip') return previous ?? null;
755
738
  return null;
756
739
  }
757
740
 
@@ -774,7 +757,7 @@ export function create_client({ target }) {
774
757
  errors.forEach((loader) => loader?.().catch(() => {}));
775
758
  loaders.forEach((loader) => loader?.[1]().catch(() => {}));
776
759
 
777
- /** @type {import('types').ServerData | null} */
760
+ /** @type {import('types').ServerNodesResponse | import('types').ServerRedirectNode | null} */
778
761
  let server_data = null;
779
762
 
780
763
  const url_changed = current.url ? id !== current.url.pathname + current.url.search : false;
@@ -981,7 +964,7 @@ export function create_client({ target }) {
981
964
  /** @type {import('types').ServerDataNode | null} */
982
965
  let server_data_node = null;
983
966
 
984
- const default_layout_has_server_load = server_loads[0] === 0;
967
+ const default_layout_has_server_load = app.server_loads[0] === 0;
985
968
 
986
969
  if (default_layout_has_server_load) {
987
970
  // TODO post-https://github.com/sveltejs/kit/discussions/6124 we can use
@@ -1311,6 +1294,21 @@ export function create_client({ target }) {
1311
1294
  after_navigate();
1312
1295
  }
1313
1296
 
1297
+ /**
1298
+ * @param {unknown} error
1299
+ * @param {import('types').NavigationEvent} event
1300
+ * @returns {import('types').MaybePromise<App.Error>}
1301
+ */
1302
+ function handle_error(error, event) {
1303
+ if (error instanceof HttpError) {
1304
+ return error.body;
1305
+ }
1306
+ return (
1307
+ app.hooks.handleError({ error, event }) ??
1308
+ /** @type {any} */ ({ message: event.route.id != null ? 'Internal Error' : 'Not Found' })
1309
+ );
1310
+ }
1311
+
1314
1312
  return {
1315
1313
  after_navigate: (fn) => {
1316
1314
  onMount(() => {
@@ -1710,9 +1708,13 @@ export function create_client({ target }) {
1710
1708
  try {
1711
1709
  const branch_promises = node_ids.map(async (n, i) => {
1712
1710
  const server_data_node = server_data_nodes[i];
1711
+ // Type isn't completely accurate, we still need to deserialize uses
1712
+ if (server_data_node?.uses) {
1713
+ server_data_node.uses = deserialize_uses(server_data_node.uses);
1714
+ }
1713
1715
 
1714
1716
  return load_node({
1715
- loader: nodes[n],
1717
+ loader: app.nodes[n],
1716
1718
  url,
1717
1719
  params,
1718
1720
  route,
@@ -1760,7 +1762,7 @@ export function create_client({ target }) {
1760
1762
  /**
1761
1763
  * @param {URL} url
1762
1764
  * @param {boolean[]} invalid
1763
- * @returns {Promise<import('types').ServerData>}
1765
+ * @returns {Promise<import('types').ServerNodesResponse |import('types').ServerRedirectNode>}
1764
1766
  */
1765
1767
  async function load_data(url, invalid) {
1766
1768
  const data_url = new URL(url);
@@ -1774,44 +1776,98 @@ async function load_data(url, invalid) {
1774
1776
  );
1775
1777
 
1776
1778
  const res = await native_fetch(data_url.href);
1777
- const data = await res.json();
1778
1779
 
1779
1780
  if (!res.ok) {
1780
1781
  // error message is a JSON-stringified string which devalue can't handle at the top level
1781
1782
  // turn it into a HttpError to not call handleError on the client again (was already handled on the server)
1782
- throw new HttpError(res.status, data);
1783
+ throw new HttpError(res.status, await res.json());
1783
1784
  }
1784
1785
 
1785
- // revive devalue-flattened data
1786
- data.nodes?.forEach((/** @type {any} */ node) => {
1787
- if (node?.type === 'data') {
1788
- node.data = devalue.unflatten(node.data);
1789
- node.uses = {
1790
- dependencies: new Set(node.uses.dependencies ?? []),
1791
- params: new Set(node.uses.params ?? []),
1792
- parent: !!node.uses.parent,
1793
- route: !!node.uses.route,
1794
- url: !!node.uses.url
1795
- };
1786
+ return new Promise(async (resolve) => {
1787
+ /**
1788
+ * Map of deferred promises that will be resolved by a subsequent chunk of data
1789
+ * @type {Map<string, import('types').Deferred>}
1790
+ */
1791
+ const deferreds = new Map();
1792
+ const reader = /** @type {ReadableStream<Uint8Array>} */ (res.body).getReader();
1793
+ const decoder = new TextDecoder();
1794
+
1795
+ /**
1796
+ * @param {any} data
1797
+ */
1798
+ function deserialize(data) {
1799
+ return devalue.unflatten(data, {
1800
+ Promise: (id) => {
1801
+ return new Promise((fulfil, reject) => {
1802
+ deferreds.set(id, { fulfil, reject });
1803
+ });
1804
+ }
1805
+ });
1806
+ }
1807
+
1808
+ let text = '';
1809
+
1810
+ while (true) {
1811
+ // Format follows ndjson (each line is a JSON object) or regular JSON spec
1812
+ const { done, value } = await reader.read();
1813
+ if (done && !text) break;
1814
+
1815
+ text += !value && text ? '\n' : decoder.decode(value); // no value -> final chunk -> add a new line to trigger the last parse
1816
+
1817
+ while (true) {
1818
+ const split = text.indexOf('\n');
1819
+ if (split === -1) {
1820
+ break;
1821
+ }
1822
+
1823
+ const node = JSON.parse(text.slice(0, split));
1824
+ text = text.slice(split + 1);
1825
+
1826
+ if (node.type === 'redirect') {
1827
+ return resolve(node);
1828
+ }
1829
+
1830
+ if (node.type === 'data') {
1831
+ // This is the first (and possibly only, if no pending promises) chunk
1832
+ node.nodes?.forEach((/** @type {any} */ node) => {
1833
+ if (node?.type === 'data') {
1834
+ node.uses = deserialize_uses(node.uses);
1835
+ node.data = deserialize(node.data);
1836
+ }
1837
+ });
1838
+
1839
+ resolve(node);
1840
+ } else if (node.type === 'chunk') {
1841
+ // This is a subsequent chunk containing deferred data
1842
+ const { id, data, error } = node;
1843
+ const deferred = /** @type {import('types').Deferred} */ (deferreds.get(id));
1844
+ deferreds.delete(id);
1845
+
1846
+ if (error) {
1847
+ deferred.reject(deserialize(error));
1848
+ } else {
1849
+ deferred.fulfil(deserialize(data));
1850
+ }
1851
+ }
1852
+ }
1796
1853
  }
1797
1854
  });
1798
1855
 
1799
- return data;
1856
+ // TODO edge case handling necessary? stream() read fails?
1800
1857
  }
1801
1858
 
1802
1859
  /**
1803
- * @param {unknown} error
1804
- * @param {import('types').NavigationEvent} event
1805
- * @returns {import('../../../types/private.js').MaybePromise<App.Error>}
1860
+ * @param {any} uses
1861
+ * @return {import('types').Uses}
1806
1862
  */
1807
- function handle_error(error, event) {
1808
- if (error instanceof HttpError) {
1809
- return error.body;
1810
- }
1811
- return (
1812
- hooks.handleError({ error, event }) ??
1813
- /** @type {any} */ ({ message: event.route.id != null ? 'Internal Error' : 'Not Found' })
1814
- );
1863
+ function deserialize_uses(uses) {
1864
+ return {
1865
+ dependencies: new Set(uses?.dependencies ?? []),
1866
+ params: new Set(uses?.params ?? []),
1867
+ parent: !!uses?.parent,
1868
+ route: !!uses?.route,
1869
+ url: !!uses?.url
1870
+ };
1815
1871
  }
1816
1872
 
1817
1873
  function reset_focus() {
@@ -1,13 +1,10 @@
1
1
  import { exec, parse_route_id } from '../../utils/routing.js';
2
2
 
3
3
  /**
4
- * @param {import('types').CSRPageNodeLoader[]} nodes
5
- * @param {number[]} server_loads
6
- * @param {typeof import('__CLIENT__/manifest.js').dictionary} dictionary
7
- * @param {Record<string, (param: string) => boolean>} matchers
4
+ * @param {import('./types').SvelteKitApp} app
8
5
  * @returns {import('types').CSRRoute[]}
9
6
  */
10
- export function parse(nodes, server_loads, dictionary, matchers) {
7
+ export function parse({ nodes, server_loads, dictionary, matchers }) {
11
8
  const layouts_with_server_load = new Set(server_loads);
12
9
 
13
10
  return Object.entries(dictionary).map(([id, [leaf, layouts, errors]]) => {
@@ -1,31 +1,20 @@
1
1
  import { DEV } from 'esm-env';
2
2
  import { create_client } from './client.js';
3
3
  import { init } from './singletons.js';
4
- import { set_assets, set_version, set_public_env } from '../shared.js';
5
4
 
6
5
  /**
7
- * @param {{
8
- * assets: string;
9
- * env: Record<string, string>;
10
- * hydrate: Parameters<import('./types').Client['_hydrate']>[0];
11
- * target: HTMLElement;
12
- * version: string;
13
- * }} opts
6
+ * @param {import('./types').SvelteKitApp} app
7
+ * @param {HTMLElement} target
8
+ * @param {Parameters<import('./types').Client['_hydrate']>[0]} [hydrate]
14
9
  */
15
- export async function start({ assets, env, hydrate, target, version }) {
16
- set_public_env(env);
17
- set_assets(assets);
18
- set_version(version);
19
-
10
+ export async function start(app, target, hydrate) {
20
11
  if (DEV && target === document.body) {
21
12
  console.warn(
22
13
  `Placing %sveltekit.body% directly inside <body> is not recommended, as your app may break for users who have certain browser extensions installed.\n\nConsider wrapping it in an element:\n\n<div style="display: contents">\n %sveltekit.body%\n</div>`
23
14
  );
24
15
  }
25
16
 
26
- const client = create_client({
27
- target
28
- });
17
+ const client = create_client(app, target);
29
18
 
30
19
  init({ client });
31
20
 
@@ -9,7 +9,43 @@ import {
9
9
  preloadData
10
10
  } from '$app/navigation';
11
11
  import { SvelteComponent } from 'svelte';
12
- import { CSRPageNode, CSRPageNodeLoader, CSRRoute, Page, TrailingSlash, Uses } from 'types';
12
+ import {
13
+ ClientHooks,
14
+ CSRPageNode,
15
+ CSRPageNodeLoader,
16
+ CSRRoute,
17
+ Page,
18
+ ParamMatcher,
19
+ TrailingSlash,
20
+ Uses
21
+ } from 'types';
22
+
23
+ export interface SvelteKitApp {
24
+ /**
25
+ * A list of all the error/layout/page nodes used in the app
26
+ */
27
+ nodes: CSRPageNodeLoader[];
28
+
29
+ /**
30
+ * A list of all layout node ids that have a server load function.
31
+ * Pages are not present because it's shorter to encode it on the leaf itself.
32
+ */
33
+ server_loads: number[];
34
+
35
+ /**
36
+ * A map of `[routeId: string]: [leaf, layouts, errors]` tuples, which
37
+ * is parsed into an array of routes on startup. The numbers refer to the indices in `nodes`.
38
+ * If the leaf number is negative, it means it does use a server load function and the complement is the node index.
39
+ * The route layout and error nodes are not referenced, they are always number 0 and 1 and always apply.
40
+ */
41
+ dictionary: Record<string, [leaf: number, layouts: number[], errors?: number[]]>;
42
+
43
+ matchers: Record<string, ParamMatcher>;
44
+
45
+ hooks: ClientHooks;
46
+
47
+ root: typeof SvelteComponent;
48
+ }
13
49
 
14
50
  export interface Client {
15
51
  // public API, exposed via $app/navigation
@@ -1,7 +1,7 @@
1
1
  import { BROWSER, DEV } from 'esm-env';
2
2
  import { writable } from 'svelte/store';
3
3
  import { assets } from '__sveltekit/paths';
4
- import { version } from '../shared.js';
4
+ import { version } from '__sveltekit/environment';
5
5
  import { PRELOAD_PRIORITIES } from './constants.js';
6
6
 
7
7
  /* global __SVELTEKIT_APP_VERSION_FILE__, __SVELTEKIT_APP_VERSION_POLL_INTERVAL__ */
@@ -1 +1 @@
1
- export { private_env as env } from '../../shared.js';
1
+ export { private_env as env } from '../../shared-server.js';
@@ -1 +1 @@
1
- export { public_env as env } from '../../shared.js';
1
+ export { public_env as env } from '../../shared-server.js';
@@ -2,9 +2,11 @@ import { HttpError, Redirect } from '../../control.js';
2
2
  import { normalize_error } from '../../../utils/error.js';
3
3
  import { once } from '../../../utils/functions.js';
4
4
  import { load_server_data } from '../page/load_data.js';
5
- import { clarify_devalue_error, handle_error_and_jsonify, serialize_data_node } from '../utils.js';
5
+ import { clarify_devalue_error, handle_error_and_jsonify, stringify_uses } from '../utils.js';
6
6
  import { normalize_path } from '../../../utils/url.js';
7
7
  import { text } from '../../../exports/index.js';
8
+ import * as devalue from 'devalue';
9
+ import { create_async_iterator } from '../../../utils/streaming.js';
8
10
 
9
11
  export const INVALIDATED_PARAM = 'x-sveltekit-invalidated';
10
12
 
@@ -58,6 +60,7 @@ export async function render_data(
58
60
 
59
61
  // == because it could be undefined (in dev) or null (in build, because of JSON.stringify)
60
62
  const node = n == undefined ? n : await manifest._.nodes[n]();
63
+ // load this. for the child, return as is. for the final result, stream things
61
64
  return load_server_data({
62
65
  event: new_event,
63
66
  state,
@@ -114,33 +117,50 @@ export async function render_data(
114
117
  )
115
118
  );
116
119
 
117
- try {
118
- const stubs = nodes.slice(0, length).map(serialize_data_node);
120
+ const { data, chunks } = get_data_json(event, options, nodes);
119
121
 
120
- const json = `{"type":"data","nodes":[${stubs.join(',')}]}`;
121
- return json_response(json);
122
- } catch (e) {
123
- const error = /** @type {any} */ (e);
124
- return json_response(JSON.stringify(clarify_devalue_error(event, error)), 500);
122
+ if (!chunks) {
123
+ // use a normal JSON response where possible, so we get `content-length`
124
+ // and can use browser JSON devtools for easier inspecting
125
+ return json_response(data);
125
126
  }
127
+
128
+ return new Response(
129
+ new ReadableStream({
130
+ async start(controller) {
131
+ controller.enqueue(data);
132
+ for await (const chunk of chunks) {
133
+ controller.enqueue(chunk);
134
+ }
135
+ controller.close();
136
+ }
137
+ }),
138
+ {
139
+ headers: {
140
+ // text/plain isn't strictly correct, but it makes it easier to inspect
141
+ // the data, and doesn't affect how it is consumed by the client
142
+ 'content-type': 'text/plain',
143
+ 'cache-control': 'private, no-store'
144
+ }
145
+ }
146
+ );
126
147
  } catch (e) {
127
148
  const error = normalize_error(e);
128
149
 
129
150
  if (error instanceof Redirect) {
130
151
  return redirect_json_response(error);
131
152
  } else {
132
- // TODO make it clearer that this was an unexpected error
133
- return json_response(JSON.stringify(await handle_error_and_jsonify(event, options, error)));
153
+ return json_response(await handle_error_and_jsonify(event, options, error), 500);
134
154
  }
135
155
  }
136
156
  }
137
157
 
138
158
  /**
139
- * @param {string} json
159
+ * @param {Record<string, any> | string} json
140
160
  * @param {number} [status]
141
161
  */
142
162
  function json_response(json, status = 200) {
143
- return text(json, {
163
+ return text(typeof json === 'string' ? json : JSON.stringify(json), {
144
164
  status,
145
165
  headers: {
146
166
  'content-type': 'application/json',
@@ -153,10 +173,90 @@ function json_response(json, status = 200) {
153
173
  * @param {Redirect} redirect
154
174
  */
155
175
  export function redirect_json_response(redirect) {
156
- return json_response(
157
- JSON.stringify({
158
- type: 'redirect',
159
- location: redirect.location
160
- })
161
- );
176
+ return json_response({
177
+ type: 'redirect',
178
+ location: redirect.location
179
+ });
180
+ }
181
+
182
+ /**
183
+ * If the serialized data contains promises, `chunks` will be an
184
+ * async iterable containing their resolutions
185
+ * @param {import('types').RequestEvent} event
186
+ * @param {import('types').SSROptions} options
187
+ * @param {Array<import('types').ServerDataSkippedNode | import('types').ServerDataNode | import('types').ServerErrorNode | null | undefined>} nodes
188
+ * @returns {{ data: string, chunks: AsyncIterable<string> | null }}
189
+ */
190
+ export function get_data_json(event, options, nodes) {
191
+ let promise_id = 1;
192
+ let count = 0;
193
+
194
+ const { iterator, push, done } = create_async_iterator();
195
+
196
+ const reducers = {
197
+ /** @param {any} thing */
198
+ Promise: (thing) => {
199
+ if (typeof thing?.then === 'function') {
200
+ const id = promise_id++;
201
+ count += 1;
202
+
203
+ /** @type {'data' | 'error'} */
204
+ let key = 'data';
205
+
206
+ thing
207
+ .catch(
208
+ /** @param {any} e */ async (e) => {
209
+ key = 'error';
210
+ return handle_error_and_jsonify(event, options, /** @type {any} */ (e));
211
+ }
212
+ )
213
+ .then(
214
+ /** @param {any} value */
215
+ async (value) => {
216
+ let str;
217
+ try {
218
+ str = devalue.stringify(value, reducers);
219
+ } catch (e) {
220
+ const error = await handle_error_and_jsonify(
221
+ event,
222
+ options,
223
+ new Error(`Failed to serialize promise while rendering ${event.route.id}`)
224
+ );
225
+
226
+ key = 'error';
227
+ str = devalue.stringify(error, reducers);
228
+ }
229
+
230
+ count -= 1;
231
+
232
+ push(`{"type":"chunk","id":${id},"${key}":${str}}\n`);
233
+ if (count === 0) done();
234
+ }
235
+ );
236
+
237
+ return id;
238
+ }
239
+ }
240
+ };
241
+
242
+ try {
243
+ const strings = nodes.map((node) => {
244
+ if (!node) return 'null';
245
+
246
+ if (node.type === 'error' || node.type === 'skip') {
247
+ return JSON.stringify(node);
248
+ }
249
+
250
+ return `{"type":"data","data":${devalue.stringify(node.data, reducers)},${stringify_uses(
251
+ node
252
+ )}${node.slash ? `,"slash":${JSON.stringify(node.slash)}` : ''}}`;
253
+ });
254
+
255
+ return {
256
+ data: `{"type":"data","nodes":[${strings.join(',')}]}\n`,
257
+ chunks: count > 0 ? iterator : null
258
+ };
259
+ } catch (e) {
260
+ throw new Error(clarify_devalue_error(event, /** @type {any} */ (e)));
261
+ }
162
262
  }
@@ -1,5 +1,5 @@
1
1
  import { respond } from './respond.js';
2
- import { set_private_env, set_public_env } from '../shared.js';
2
+ import { set_private_env, set_public_env } from '../shared-server.js';
3
3
  import { options, get_hooks } from '__SERVER__/internal.js';
4
4
 
5
5
  export class Server {
@@ -3,12 +3,7 @@ import { compact } from '../../../utils/array.js';
3
3
  import { normalize_error } from '../../../utils/error.js';
4
4
  import { add_data_suffix } from '../../../utils/url.js';
5
5
  import { HttpError, Redirect } from '../../control.js';
6
- import {
7
- redirect_response,
8
- static_error_page,
9
- handle_error_and_jsonify,
10
- serialize_data_node
11
- } from '../utils.js';
6
+ import { redirect_response, static_error_page, handle_error_and_jsonify } from '../utils.js';
12
7
  import {
13
8
  handle_action_json_request,
14
9
  handle_action_request,
@@ -19,6 +14,7 @@ import { load_data, load_server_data } from './load_data.js';
19
14
  import { render_response } from './render.js';
20
15
  import { respond_with_error } from './respond_with_error.js';
21
16
  import { get_option } from '../../../utils/options.js';
17
+ import { get_data_json } from '../data/index.js';
22
18
 
23
19
  /**
24
20
  * @param {import('types').RequestEvent} event
@@ -290,13 +286,22 @@ export async function render_page(event, route, page, options, manifest, state,
290
286
  }
291
287
 
292
288
  if (state.prerendering && should_prerender_data) {
293
- const body = `{"type":"data","nodes":[${branch
294
- .map((node) => serialize_data_node(node?.server_data))
295
- .join(',')}]}`;
289
+ // ndjson format
290
+ let { data, chunks } = get_data_json(
291
+ event,
292
+ options,
293
+ branch.map((node) => node?.server_data)
294
+ );
295
+
296
+ if (chunks) {
297
+ for await (const chunk of chunks) {
298
+ data += chunk;
299
+ }
300
+ }
296
301
 
297
302
  state.prerendering.dependencies.set(data_pathname, {
298
- response: text(body),
299
- body
303
+ response: text(data),
304
+ body: data
300
305
  });
301
306
  }
302
307