Skip to content

Commit a3a10b2

Browse files
author
Rich Harris
authored
lay more groundwork for #519/#626 (#882)
* load all components up-front * typechecking * invoke router on startup if no SSR * getting closer * tests passing * remove trial script * fix unsubscription * lint * rename some stuff
1 parent 26f4f2c commit a3a10b2

File tree

9 files changed

+675
-526
lines changed

9 files changed

+675
-526
lines changed

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,6 @@ class Watcher extends EventEmitter {
3535
this.https = https;
3636
this.config = config;
3737

38-
process.env.NODE_ENV = 'development';
39-
4038
process.on('exit', () => {
4139
this.close();
4240
});

packages/kit/src/runtime/client/start.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ import { set_paths } from '../paths.js';
1818
* status: number;
1919
* host: string;
2020
* route: boolean;
21+
* spa: boolean;
2122
* hydrate: import('./types').NavigationCandidate;
2223
* }} opts */
23-
export async function start({ paths, target, session, host, route, hydrate }) {
24+
export async function start({ paths, target, session, host, route, spa, hydrate }) {
2425
const router =
2526
route &&
2627
new Router({
@@ -42,6 +43,8 @@ export async function start({ paths, target, session, host, route, hydrate }) {
4243
if (hydrate) await renderer.start(hydrate);
4344
if (route) router.init(renderer);
4445

46+
if (spa) router.goto(location.href, { replaceState: true }, []);
47+
4548
dispatchEvent(new CustomEvent('sveltekit:start'));
4649
}
4750

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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { respond } from './respond.js';
2+
import { respond_with_error } from './respond_with_error.js';
23

34
/**
45
* @param {import('types').Request} request
@@ -23,9 +24,7 @@ export default async function render_page(request, route, options) {
2324
request,
2425
options,
2526
$session,
26-
route,
27-
status: 200,
28-
error: null
27+
route
2928
});
3029

3130
if (response) {
@@ -44,11 +43,10 @@ export default async function render_page(request, route, options) {
4443
};
4544
}
4645
} else {
47-
return await respond({
46+
return await respond_with_error({
4847
request,
4948
options,
5049
$session,
51-
route,
5250
status: 404,
5351
error: new Error(`Not found: ${request.path}`)
5452
});
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
import fetch, { Response } from 'node-fetch';
2+
import { parse, resolve, URLSearchParams } from 'url';
3+
import { normalize } from '../../load.js';
4+
import { ssr } from '../index.js';
5+
6+
const s = JSON.stringify;
7+
8+
/**
9+
*
10+
* @param {{
11+
* request: import('types').Request;
12+
* options: import('types.internal').SSRRenderOptions;
13+
* route: import('types.internal').SSRPage;
14+
* page: import('types.internal').Page;
15+
* node: import('types.internal').SSRNode;
16+
* $session: any;
17+
* context: Record<string, any>;
18+
* is_leaf: boolean;
19+
* }} opts
20+
* @returns {Promise<import('./types').Loaded>}
21+
*/
22+
export async function load_node({
23+
request,
24+
options,
25+
route,
26+
page,
27+
node,
28+
$session,
29+
context,
30+
is_leaf
31+
}) {
32+
const { module } = node;
33+
34+
let uses_credentials = false;
35+
36+
/** @type {Array<{
37+
* url: string;
38+
* json: string;
39+
* }>} */
40+
const fetched = [];
41+
42+
const loaded = module.load
43+
? await module.load.call(null, {
44+
page,
45+
get session() {
46+
uses_credentials = true;
47+
return $session;
48+
},
49+
/**
50+
* @param {RequestInfo} resource
51+
* @param {RequestInit} opts
52+
*/
53+
fetch: async (resource, opts = {}) => {
54+
/** @type {string} */
55+
let url;
56+
57+
if (typeof resource === 'string') {
58+
url = resource;
59+
} else {
60+
url = resource.url;
61+
62+
opts = {
63+
method: resource.method,
64+
headers: resource.headers,
65+
body: resource.body,
66+
mode: resource.mode,
67+
credentials: resource.credentials,
68+
cache: resource.cache,
69+
redirect: resource.redirect,
70+
referrer: resource.referrer,
71+
integrity: resource.integrity,
72+
...opts
73+
};
74+
}
75+
76+
if (options.local && url.startsWith(options.paths.assets)) {
77+
// when running `start`, or prerendering, `assets` should be
78+
// config.kit.paths.assets, but we should still be able to fetch
79+
// assets directly from `static`
80+
url = url.replace(options.paths.assets, '');
81+
}
82+
83+
const parsed = parse(url);
84+
85+
let response;
86+
87+
if (parsed.protocol) {
88+
// external fetch
89+
response = await fetch(
90+
parsed.href,
91+
/** @type {import('node-fetch').RequestInit} */ (opts)
92+
);
93+
} else {
94+
// otherwise we're dealing with an internal fetch
95+
const resolved = resolve(request.path, parsed.pathname);
96+
97+
// handle fetch requests for static assets. e.g. prebaked data, etc.
98+
// we need to support everything the browser's fetch supports
99+
const filename = resolved.slice(1);
100+
const filename_html = `${filename}/index.html`; // path may also match path/index.html
101+
const asset = options.manifest.assets.find(
102+
(d) => d.file === filename || d.file === filename_html
103+
);
104+
105+
if (asset) {
106+
// we don't have a running server while prerendering because jumping between
107+
// processes would be inefficient so we have get_static_file instead
108+
if (options.get_static_file) {
109+
response = new Response(options.get_static_file(asset.file), {
110+
headers: {
111+
'content-type': asset.type
112+
}
113+
});
114+
} else {
115+
// TODO we need to know what protocol to use
116+
response = await fetch(
117+
`http://${page.host}/${asset.file}`,
118+
/** @type {import('node-fetch').RequestInit} */ (opts)
119+
);
120+
}
121+
}
122+
123+
if (!response) {
124+
const headers = /** @type {import('types.internal').Headers} */ ({ ...opts.headers });
125+
126+
// TODO: fix type https://github.com/node-fetch/node-fetch/issues/1113
127+
if (opts.credentials !== 'omit') {
128+
uses_credentials = true;
129+
130+
headers.cookie = request.headers.cookie;
131+
132+
if (!headers.authorization) {
133+
headers.authorization = request.headers.authorization;
134+
}
135+
}
136+
137+
const rendered = await ssr(
138+
{
139+
host: request.host,
140+
method: opts.method || 'GET',
141+
headers,
142+
path: resolved,
143+
body: /** @type {any} */ (opts.body),
144+
query: new URLSearchParams(parsed.query || '')
145+
},
146+
{
147+
...options,
148+
fetched: url,
149+
initiator: route
150+
}
151+
);
152+
153+
if (rendered) {
154+
if (options.dependencies) {
155+
options.dependencies.set(resolved, rendered);
156+
}
157+
158+
response = new Response(rendered.body, {
159+
status: rendered.status,
160+
headers: rendered.headers
161+
});
162+
}
163+
}
164+
}
165+
166+
if (response) {
167+
const proxy = new Proxy(response, {
168+
get(response, key, receiver) {
169+
async function text() {
170+
const body = await response.text();
171+
172+
/** @type {import('types.internal').Headers} */
173+
const headers = {};
174+
response.headers.forEach((value, key) => {
175+
if (key !== 'etag' && key !== 'set-cookie') headers[key] = value;
176+
});
177+
178+
// prettier-ignore
179+
fetched.push({
180+
url,
181+
json: `{"status":${response.status},"statusText":${s(response.statusText)},"headers":${s(headers)},"body":${escape(body)}}`
182+
});
183+
184+
return body;
185+
}
186+
187+
if (key === 'text') {
188+
return text;
189+
}
190+
191+
if (key === 'json') {
192+
return async () => {
193+
return JSON.parse(await text());
194+
};
195+
}
196+
197+
// TODO arrayBuffer?
198+
199+
return Reflect.get(response, key, receiver);
200+
}
201+
});
202+
203+
return proxy;
204+
}
205+
206+
return (
207+
response ||
208+
new Response('Not found', {
209+
status: 404
210+
})
211+
);
212+
},
213+
context: { ...context }
214+
})
215+
: {};
216+
217+
// if leaf node (i.e. page component) has a load function
218+
// that returns nothing, we fall through to the next one
219+
if (!loaded && is_leaf) return;
220+
221+
return {
222+
node,
223+
loaded: normalize(loaded),
224+
fetched,
225+
uses_credentials
226+
};
227+
}
228+
229+
/** @type {Record<string, string>} */
230+
const escaped = {
231+
'<': '\\u003C',
232+
'>': '\\u003E',
233+
'/': '\\u002F',
234+
'\\': '\\\\',
235+
'\b': '\\b',
236+
'\f': '\\f',
237+
'\n': '\\n',
238+
'\r': '\\r',
239+
'\t': '\\t',
240+
'\0': '\\0',
241+
'\u2028': '\\u2028',
242+
'\u2029': '\\u2029'
243+
};
244+
245+
/** @param {string} str */
246+
function escape(str) {
247+
let result = '"';
248+
249+
for (let i = 0; i < str.length; i += 1) {
250+
const char = str.charAt(i);
251+
const code = char.charCodeAt(0);
252+
253+
if (char === '"') {
254+
result += '\\"';
255+
} else if (char in escaped) {
256+
result += escaped[char];
257+
} else if (code >= 0xd800 && code <= 0xdfff) {
258+
const next = str.charCodeAt(i + 1);
259+
260+
// If this is the beginning of a [high, low] surrogate pair,
261+
// add the next two characters, otherwise escape
262+
if (code <= 0xdbff && next >= 0xdc00 && next <= 0xdfff) {
263+
result += char + str[++i];
264+
} else {
265+
result += `\\u${code.toString(16).toUpperCase()}`;
266+
}
267+
} else {
268+
result += char;
269+
}
270+
}
271+
272+
result += '"';
273+
return result;
274+
}

0 commit comments

Comments
 (0)