@sveltejs/kit 1.0.0-next.343 → 1.0.0-next.346

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.
@@ -1,22 +1,21 @@
1
- import path__default from 'path';
2
- import { svelte } from '@sveltejs/vite-plugin-svelte';
1
+ import fs__default, { readFileSync, writeFileSync } from 'fs';
2
+ import path__default, { join, dirname } from 'path';
3
+ import { p as posixify, m as mkdirp, r as rimraf } from './filesystem.js';
4
+ import { all } from './sync.js';
5
+ import { g as get_aliases, p as print_config_conflicts, r as resolve_entry, a as get_runtime_path, l as load_template } from '../cli.js';
6
+ import { g as generate_manifest } from './index2.js';
3
7
  import * as vite from 'vite';
8
+ import { s } from './misc.js';
4
9
  import { d as deep_merge } from './object.js';
5
- import { g as get_runtime_path, r as resolve_entry, $, l as load_template, c as coalesce_to_error, a as get_mime_lookup, b as load_config, d as get_aliases, p as print_config_conflicts } from '../cli.js';
6
- import fs__default from 'fs';
7
- import { URL } from 'url';
8
- import { S as SVELTE_KIT_ASSETS, s as sirv } from './constants.js';
10
+ import { svelte } from '@sveltejs/vite-plugin-svelte';
11
+ import { pathToFileURL, URL as URL$1 } from 'url';
9
12
  import { installPolyfills } from '../node/polyfills.js';
10
- import { update, init } from './sync.js';
11
- import { getRequest, setResponse } from '../node.js';
12
- import { p as posixify } from './filesystem.js';
13
- import { p as parse_route_id } from './misc.js';
14
- import 'sade';
13
+ import './write_tsconfig.js';
14
+ import 'chokidar';
15
15
  import 'child_process';
16
16
  import 'net';
17
- import 'chokidar';
17
+ import 'sade';
18
18
  import 'os';
19
- import 'querystring';
20
19
  import 'node:http';
21
20
  import 'node:https';
22
21
  import 'node:zlib';
@@ -24,523 +23,1334 @@ import 'node:stream';
24
23
  import 'node:util';
25
24
  import 'node:url';
26
25
  import 'crypto';
27
- import './write_tsconfig.js';
28
- import 'stream';
29
26
 
30
- // Vite doesn't expose this so we just copy the list for now
31
- // https://github.com/vitejs/vite/blob/3edd1af56e980aef56641a5a51cf2932bb580d41/packages/vite/src/node/plugins/css.ts#L96
32
- const style_pattern = /\.(css|less|sass|scss|styl|stylus|pcss|postcss)$/;
27
+ const absolute = /^([a-z]+:)?\/?\//;
28
+ const scheme = /^[a-z]+:/;
29
+
30
+ /**
31
+ * @param {string} base
32
+ * @param {string} path
33
+ */
34
+ function resolve(base, path) {
35
+ if (scheme.test(path)) return path;
36
+
37
+ const base_match = absolute.exec(base);
38
+ const path_match = absolute.exec(path);
33
39
 
34
- const cwd$1 = process.cwd();
40
+ if (!base_match) {
41
+ throw new Error(`bad base path: "${base}"`);
42
+ }
43
+
44
+ const baseparts = path_match ? [] : base.slice(base_match[0].length).split('/');
45
+ const pathparts = path_match ? path.slice(path_match[0].length).split('/') : path.split('/');
46
+
47
+ baseparts.pop();
48
+
49
+ for (let i = 0; i < pathparts.length; i += 1) {
50
+ const part = pathparts[i];
51
+ if (part === '.') continue;
52
+ else if (part === '..') baseparts.pop();
53
+ else baseparts.push(part);
54
+ }
55
+
56
+ const prefix = (path_match && path_match[0]) || (base_match && base_match[0]) || '';
57
+
58
+ return `${prefix}${baseparts.join('/')}`;
59
+ }
60
+
61
+ /** @param {string} path */
62
+ function is_root_relative(path) {
63
+ return path[0] === '/' && path[1] !== '/';
64
+ }
35
65
 
36
66
  /**
37
- * @param {import('types').ValidatedConfig} config
38
- * @returns {Promise<import('vite').Plugin>}
67
+ * @param {string} path
68
+ * @param {import('types').TrailingSlash} trailing_slash
39
69
  */
40
- async function create_plugin(config) {
41
- const runtime = get_runtime_path(config);
70
+ function normalize_path(path, trailing_slash) {
71
+ if (path === '/' || trailing_slash === 'ignore') return path;
42
72
 
43
- process.env.VITE_SVELTEKIT_APP_VERSION_POLL_INTERVAL = '0';
73
+ if (trailing_slash === 'never') {
74
+ return path.endsWith('/') ? path.slice(0, -1) : path;
75
+ } else if (trailing_slash === 'always' && !path.endsWith('/')) {
76
+ return path + '/';
77
+ }
44
78
 
45
- /** @type {import('types').Respond} */
46
- const respond = (await import(`${runtime}/server/index.js`)).respond;
79
+ return path;
80
+ }
47
81
 
82
+ /**
83
+ * @typedef {import('rollup').RollupOutput} RollupOutput
84
+ * @typedef {import('rollup').OutputChunk} OutputChunk
85
+ * @typedef {import('rollup').OutputAsset} OutputAsset
86
+ */
87
+
88
+ /** @param {import('vite').UserConfig} config */
89
+ async function create_build(config) {
90
+ const { output } = /** @type {RollupOutput} */ (await vite.build(config));
91
+
92
+ const chunks = output.filter(
93
+ /** @returns {output is OutputChunk} */ (output) => output.type === 'chunk'
94
+ );
95
+
96
+ const assets = output.filter(
97
+ /** @returns {output is OutputAsset} */ (output) => output.type === 'asset'
98
+ );
99
+
100
+ return { chunks, assets };
101
+ }
102
+
103
+ /**
104
+ * @param {string} file
105
+ * @param {import('vite').Manifest} manifest
106
+ * @param {Set<string>} css
107
+ * @param {Set<string>} js
108
+ */
109
+ function find_deps(file, manifest, js, css) {
110
+ const chunk = manifest[file];
111
+
112
+ if (js.has(chunk.file)) return;
113
+ js.add(chunk.file);
114
+
115
+ if (chunk.css) {
116
+ chunk.css.forEach((file) => css.add(file));
117
+ }
118
+
119
+ if (chunk.imports) {
120
+ chunk.imports.forEach((file) => find_deps(file, manifest, js, css));
121
+ }
122
+ }
123
+
124
+ /**
125
+ * @param {{
126
+ * client_out_dir?: string;
127
+ * config: import('types').ValidatedConfig;
128
+ * input: Record<string, string>;
129
+ * output_dir: string;
130
+ * ssr: boolean;
131
+ * }} options
132
+ * @return {import('vite').UserConfig}
133
+ */
134
+ const get_default_config = function ({ client_out_dir, config, input, output_dir, ssr }) {
48
135
  return {
49
- name: 'vite-plugin-svelte-kit',
50
-
51
- configureServer(vite) {
52
- installPolyfills();
53
-
54
- /** @type {import('types').SSRManifest} */
55
- let manifest;
56
-
57
- function update_manifest() {
58
- const { manifest_data } = update(config);
59
-
60
- manifest = {
61
- appDir: config.kit.appDir,
62
- assets: new Set(manifest_data.assets.map((asset) => asset.file)),
63
- mimeTypes: get_mime_lookup(manifest_data),
64
- _: {
65
- entry: {
66
- file: `/@fs${runtime}/client/start.js`,
67
- css: [],
68
- js: []
69
- },
70
- nodes: manifest_data.components.map((id) => {
71
- return async () => {
72
- const url = id.startsWith('..') ? `/@fs${path__default.posix.resolve(id)}` : `/${id}`;
73
-
74
- const module = /** @type {import('types').SSRComponent} */ (
75
- await vite.ssrLoadModule(url, { fixStacktrace: false })
76
- );
77
- const node = await vite.moduleGraph.getModuleByUrl(url);
78
-
79
- if (!node) throw new Error(`Could not find node for ${url}`);
80
-
81
- const deps = new Set();
82
- await find_deps(vite, node, deps);
83
-
84
- /** @type {Record<string, string>} */
85
- const styles = {};
86
-
87
- for (const dep of deps) {
88
- const parsed = new URL(dep.url, 'http://localhost/');
89
- const query = parsed.searchParams;
90
-
91
- if (
92
- style_pattern.test(dep.file) ||
93
- (query.has('svelte') && query.get('type') === 'style')
94
- ) {
95
- try {
96
- const mod = await vite.ssrLoadModule(dep.url, { fixStacktrace: false });
97
- styles[dep.url] = mod.default;
98
- } catch {
99
- // this can happen with dynamically imported modules, I think
100
- // because the Vite module graph doesn't distinguish between
101
- // static and dynamic imports? TODO investigate, submit fix
102
- }
103
- }
104
- }
136
+ base: assets_base(config),
137
+ build: {
138
+ cssCodeSplit: true,
139
+ manifest: true,
140
+ outDir: ssr ? `${output_dir}/server` : `${client_out_dir}/immutable`,
141
+ polyfillDynamicImport: false,
142
+ rollupOptions: {
143
+ input,
144
+ output: {
145
+ format: 'esm',
146
+ entryFileNames: ssr ? '[name].js' : '[name]-[hash].js',
147
+ chunkFileNames: 'chunks/[name]-[hash].js',
148
+ assetFileNames: 'assets/[name]-[hash][extname]'
149
+ },
150
+ preserveEntrySignatures: 'strict'
151
+ },
152
+ ssr
153
+ },
154
+ plugins: [
155
+ svelte({
156
+ ...config,
157
+ compilerOptions: {
158
+ ...config.compilerOptions,
159
+ hydratable: !!config.kit.browser.hydrate
160
+ },
161
+ configFile: false
162
+ })
163
+ ],
164
+ // prevent Vite copying the contents of `config.kit.files.assets`,
165
+ // if it happens to be 'public' instead of 'static'
166
+ publicDir: false,
167
+ resolve: {
168
+ alias: get_aliases(config)
169
+ }
170
+ };
171
+ };
105
172
 
106
- return {
107
- module,
108
- entry: url.endsWith('.svelte') ? url : url + '?import',
109
- css: [],
110
- js: [],
111
- // in dev we inline all styles to avoid FOUC
112
- styles
113
- };
114
- };
115
- }),
116
- routes: manifest_data.routes.map((route) => {
117
- const { pattern, names, types } = parse_route_id(route.id);
118
-
119
- if (route.type === 'page') {
120
- return {
121
- type: 'page',
122
- id: route.id,
123
- pattern,
124
- names,
125
- types,
126
- shadow: route.shadow
127
- ? async () => {
128
- const url = path__default.resolve(cwd$1, /** @type {string} */ (route.shadow));
129
- return await vite.ssrLoadModule(url, { fixStacktrace: false });
130
- }
131
- : null,
132
- a: route.a.map((id) => (id ? manifest_data.components.indexOf(id) : undefined)),
133
- b: route.b.map((id) => (id ? manifest_data.components.indexOf(id) : undefined))
134
- };
135
- }
173
+ /**
174
+ * @param {import('types').ValidatedConfig} config
175
+ * @returns {string}
176
+ */
177
+ function assets_base(config) {
178
+ // TODO this is so that Vite's preloading works. Unfortunately, it fails
179
+ // during `svelte-kit preview`, because we use a local asset path. This
180
+ // may be fixed in Vite 3: https://github.com/vitejs/vite/issues/2009
181
+ const { base, assets } = config.kit.paths;
182
+ return `${assets || base}/${config.kit.appDir}/immutable/`;
183
+ }
136
184
 
137
- return {
138
- type: 'endpoint',
139
- id: route.id,
140
- pattern,
141
- names,
142
- types,
143
- load: async () => {
144
- const url = path__default.resolve(cwd$1, route.file);
145
- return await vite.ssrLoadModule(url, { fixStacktrace: false });
146
- }
147
- };
148
- }),
149
- matchers: async () => {
150
- /** @type {Record<string, import('types').ParamMatcher>} */
151
- const matchers = {};
152
-
153
- for (const key in manifest_data.matchers) {
154
- const file = manifest_data.matchers[key];
155
- const url = path__default.resolve(cwd$1, file);
156
- const module = await vite.ssrLoadModule(url, { fixStacktrace: false });
157
-
158
- if (module.match) {
159
- matchers[key] = module.match;
160
- } else {
161
- throw new Error(`${file} does not export a \`match\` function`);
162
- }
163
- }
185
+ /**
186
+ * @param {{
187
+ * config: import('types').ValidatedConfig;
188
+ * manifest_data: import('types').ManifestData;
189
+ * output_dir: string;
190
+ * service_worker_entry_file: string | null;
191
+ * }} options
192
+ * @param {import('types').Prerendered} prerendered
193
+ * @param {import('vite').Manifest} client_manifest
194
+ */
195
+ async function build_service_worker(
196
+ { config, manifest_data, output_dir, service_worker_entry_file },
197
+ prerendered,
198
+ client_manifest
199
+ ) {
200
+ const build = new Set();
201
+ for (const key in client_manifest) {
202
+ const { file, css = [], assets = [] } = client_manifest[key];
203
+ build.add(file);
204
+ css.forEach((file) => build.add(file));
205
+ assets.forEach((file) => build.add(file));
206
+ }
164
207
 
165
- return matchers;
166
- }
208
+ const service_worker = `${config.kit.outDir}/generated/service-worker.js`;
209
+
210
+ fs__default.writeFileSync(
211
+ service_worker,
212
+ `
213
+ // TODO remove for 1.0
214
+ export const timestamp = {
215
+ toString: () => {
216
+ throw new Error('\`timestamp\` has been removed from $service-worker. Use \`version\` instead');
217
+ }
218
+ };
219
+
220
+ export const build = [
221
+ ${Array.from(build)
222
+ .map((file) => `${s(`${config.kit.paths.base}/${config.kit.appDir}/immutable/${file}`)}`)
223
+ .join(',\n\t\t\t\t')}
224
+ ];
225
+
226
+ export const files = [
227
+ ${manifest_data.assets
228
+ .filter((asset) => config.kit.serviceWorker.files(asset.file))
229
+ .map((asset) => `${s(`${config.kit.paths.base}/${asset.file}`)}`)
230
+ .join(',\n\t\t\t\t')}
231
+ ];
232
+
233
+ export const prerendered = [
234
+ ${prerendered.paths
235
+ .map((path) => s(normalize_path(path, config.kit.trailingSlash)))
236
+ .join(',\n\t\t\t\t')}
237
+ ];
238
+
239
+ export const version = ${s(config.kit.version.name)};
240
+ `
241
+ .replace(/^\t{3}/gm, '')
242
+ .trim()
243
+ );
244
+
245
+ /** @type {[any, string[]]} */
246
+ const [merged_config, conflicts] = deep_merge(await config.kit.vite(), {
247
+ base: assets_base(config),
248
+ build: {
249
+ lib: {
250
+ entry: service_worker_entry_file,
251
+ name: 'app',
252
+ formats: ['es']
253
+ },
254
+ rollupOptions: {
255
+ output: {
256
+ entryFileNames: 'service-worker.js'
257
+ }
258
+ },
259
+ outDir: `${output_dir}/client`,
260
+ emptyOutDir: false
261
+ },
262
+ resolve: {
263
+ alias: {
264
+ '$service-worker': service_worker,
265
+ $lib: config.kit.files.lib
266
+ }
267
+ }
268
+ });
269
+
270
+ print_config_conflicts(conflicts, 'kit.vite.', 'build_service_worker');
271
+
272
+ await vite.build(merged_config);
273
+ }
274
+
275
+ /**
276
+ * @param {{
277
+ * cwd: string;
278
+ * config: import('types').ValidatedConfig;
279
+ * manifest_data: import('types').ManifestData;
280
+ * output_dir: string;
281
+ * client_entry_file: string;
282
+ * }} options
283
+ */
284
+ async function build_client(options) {
285
+ const { cwd, config, manifest_data, output_dir, client_entry_file } = options;
286
+
287
+ process.env.VITE_SVELTEKIT_APP_VERSION = config.kit.version.name;
288
+ process.env.VITE_SVELTEKIT_APP_VERSION_FILE = `${config.kit.appDir}/version.json`;
289
+ process.env.VITE_SVELTEKIT_APP_VERSION_POLL_INTERVAL = `${config.kit.version.pollInterval}`;
290
+
291
+ const client_out_dir = `${output_dir}/client/${config.kit.appDir}`;
292
+
293
+ /** @type {Record<string, string>} */
294
+ const input = {
295
+ start: path__default.resolve(cwd, client_entry_file)
296
+ };
297
+
298
+ // This step is optional — Vite/Rollup will create the necessary chunks
299
+ // for everything regardless — but it means that entry chunks reflect
300
+ // their location in the source code, which is helpful for debugging
301
+ manifest_data.components.forEach((file) => {
302
+ const resolved = path__default.resolve(cwd, file);
303
+ const relative = path__default.relative(config.kit.files.routes, resolved);
304
+
305
+ const name = relative.startsWith('..')
306
+ ? path__default.basename(file)
307
+ : posixify(path__default.join('pages', relative));
308
+ input[name] = resolved;
309
+ });
310
+
311
+ /** @type {[any, string[]]} */
312
+ const [merged_config, conflicts] = deep_merge(
313
+ await config.kit.vite(),
314
+ get_default_config({ ...options, client_out_dir, input, ssr: false })
315
+ );
316
+
317
+ print_config_conflicts(conflicts, 'kit.vite.', 'build_client');
318
+
319
+ const { chunks, assets } = await create_build(merged_config);
320
+
321
+ /** @type {import('vite').Manifest} */
322
+ const vite_manifest = JSON.parse(
323
+ fs__default.readFileSync(`${client_out_dir}/immutable/manifest.json`, 'utf-8')
324
+ );
325
+
326
+ const entry = posixify(client_entry_file);
327
+ const entry_js = new Set();
328
+ const entry_css = new Set();
329
+ find_deps(entry, vite_manifest, entry_js, entry_css);
330
+
331
+ fs__default.writeFileSync(
332
+ `${client_out_dir}/version.json`,
333
+ JSON.stringify({ version: process.env.VITE_SVELTEKIT_APP_VERSION })
334
+ );
335
+
336
+ return {
337
+ assets,
338
+ chunks,
339
+ entry: {
340
+ file: vite_manifest[entry].file,
341
+ js: Array.from(entry_js),
342
+ css: Array.from(entry_css)
343
+ },
344
+ vite_manifest
345
+ };
346
+ }
347
+
348
+ /**
349
+ * @param {{
350
+ * hooks: string;
351
+ * config: import('types').ValidatedConfig;
352
+ * has_service_worker: boolean;
353
+ * runtime: string;
354
+ * template: string;
355
+ * }} opts
356
+ */
357
+ const server_template = ({ config, hooks, has_service_worker, runtime, template }) => `
358
+ import root from '__GENERATED__/root.svelte';
359
+ import { respond } from '${runtime}/server/index.js';
360
+ import { set_paths, assets, base } from '${runtime}/paths.js';
361
+ import { set_prerendering } from '${runtime}/env.js';
362
+
363
+ const template = ({ head, body, assets, nonce }) => ${s(template)
364
+ .replace('%sveltekit.head%', '" + head + "')
365
+ .replace('%sveltekit.body%', '" + body + "')
366
+ .replace(/%sveltekit\.assets%/g, '" + assets + "')
367
+ .replace(/%sveltekit\.nonce%/g, '" + nonce + "')};
368
+
369
+ let read = null;
370
+
371
+ set_paths(${s(config.kit.paths)});
372
+
373
+ let default_protocol = 'https';
374
+
375
+ // allow paths to be globally overridden
376
+ // in svelte-kit preview and in prerendering
377
+ export function override(settings) {
378
+ default_protocol = settings.protocol || default_protocol;
379
+ set_paths(settings.paths);
380
+ set_prerendering(settings.prerendering);
381
+ read = settings.read;
382
+ }
383
+
384
+ export class Server {
385
+ constructor(manifest) {
386
+ this.options = {
387
+ csp: ${s(config.kit.csp)},
388
+ dev: false,
389
+ floc: ${config.kit.floc},
390
+ get_stack: error => String(error), // for security
391
+ handle_error: (error, event) => {
392
+ this.options.hooks.handleError({
393
+ error,
394
+ event,
395
+
396
+ // TODO remove for 1.0
397
+ // @ts-expect-error
398
+ get request() {
399
+ throw new Error('request in handleError has been replaced with event. See https://github.com/sveltejs/kit/pull/3384 for details');
167
400
  }
168
- };
401
+ });
402
+ error.stack = this.options.get_stack(error);
403
+ },
404
+ hooks: null,
405
+ hydrate: ${s(config.kit.browser.hydrate)},
406
+ manifest,
407
+ method_override: ${s(config.kit.methodOverride)},
408
+ paths: { base, assets },
409
+ prefix: assets + '/${config.kit.appDir}/immutable/',
410
+ prerender: {
411
+ default: ${config.kit.prerender.default},
412
+ enabled: ${config.kit.prerender.enabled}
413
+ },
414
+ read,
415
+ root,
416
+ service_worker: ${has_service_worker ? "base + '/service-worker.js'" : 'null'},
417
+ router: ${s(config.kit.browser.router)},
418
+ template,
419
+ template_contains_nonce: ${template.includes('%sveltekit.nonce%')},
420
+ trailing_slash: ${s(config.kit.trailingSlash)}
421
+ };
422
+ }
423
+
424
+ async respond(request, options = {}) {
425
+ if (!(request instanceof Request)) {
426
+ throw new Error('The first argument to server.respond must be a Request object. See https://github.com/sveltejs/kit/pull/3384 for details');
427
+ }
428
+
429
+ if (!this.options.hooks) {
430
+ const module = await import(${s(hooks)});
431
+ this.options.hooks = {
432
+ getSession: module.getSession || (() => ({})),
433
+ handle: module.handle || (({ event, resolve }) => resolve(event)),
434
+ handleError: module.handleError || (({ error }) => console.error(error.stack)),
435
+ externalFetch: module.externalFetch || fetch
436
+ };
437
+ }
438
+
439
+ return respond(request, this.options, options);
440
+ }
441
+ }
442
+ `;
443
+
444
+ /**
445
+ * @param {{
446
+ * cwd: string;
447
+ * config: import('types').ValidatedConfig
448
+ * manifest_data: import('types').ManifestData
449
+ * build_dir: string;
450
+ * output_dir: string;
451
+ * service_worker_entry_file: string | null;
452
+ * }} options
453
+ * @param {{ vite_manifest: import('vite').Manifest, assets: import('rollup').OutputAsset[] }} client
454
+ */
455
+ async function build_server(options, client) {
456
+ const { cwd, config, manifest_data, build_dir, output_dir, service_worker_entry_file } = options;
457
+
458
+ let hooks_file = resolve_entry(config.kit.files.hooks);
459
+ if (!hooks_file || !fs__default.existsSync(hooks_file)) {
460
+ hooks_file = path__default.join(config.kit.outDir, 'build/hooks.js');
461
+ fs__default.writeFileSync(hooks_file, '');
462
+ }
463
+
464
+ /** @type {Record<string, string>} */
465
+ const input = {
466
+ index: `${build_dir}/index.js`
467
+ };
468
+
469
+ // add entry points for every endpoint...
470
+ manifest_data.routes.forEach((route) => {
471
+ const file = route.type === 'endpoint' ? route.file : route.shadow;
472
+
473
+ if (file) {
474
+ const resolved = path__default.resolve(cwd, file);
475
+ const relative = path__default.relative(config.kit.files.routes, resolved);
476
+ const name = posixify(path__default.join('entries/endpoints', relative.replace(/\.js$/, '')));
477
+ input[name] = resolved;
478
+ }
479
+ });
480
+
481
+ // ...and every component used by pages...
482
+ manifest_data.components.forEach((file) => {
483
+ const resolved = path__default.resolve(cwd, file);
484
+ const relative = path__default.relative(config.kit.files.routes, resolved);
485
+
486
+ const name = relative.startsWith('..')
487
+ ? posixify(path__default.join('entries/fallbacks', path__default.basename(file)))
488
+ : posixify(path__default.join('entries/pages', relative));
489
+ input[name] = resolved;
490
+ });
491
+
492
+ // ...and every matcher
493
+ Object.entries(manifest_data.matchers).forEach(([key, file]) => {
494
+ const name = posixify(path__default.join('entries/matchers', key));
495
+ input[name] = path__default.resolve(cwd, file);
496
+ });
497
+
498
+ /** @type {(file: string) => string} */
499
+ const app_relative = (file) => {
500
+ const relative_file = path__default.relative(build_dir, path__default.resolve(cwd, file));
501
+ return relative_file[0] === '.' ? relative_file : `./${relative_file}`;
502
+ };
503
+
504
+ fs__default.writeFileSync(
505
+ input.index,
506
+ server_template({
507
+ config,
508
+ hooks: app_relative(hooks_file),
509
+ has_service_worker: config.kit.serviceWorker.register && !!service_worker_entry_file,
510
+ runtime: get_runtime_path(config),
511
+ template: load_template(cwd, config)
512
+ })
513
+ );
514
+
515
+ /** @type {import('vite').UserConfig} */
516
+ const vite_config = await config.kit.vite();
517
+
518
+ const default_config = {
519
+ build: {
520
+ target: 'node14.8'
521
+ },
522
+ ssr: {
523
+ // when developing against the Kit src code, we want to ensure that
524
+ // our dependencies are bundled so that apps don't need to install
525
+ // them as peerDependencies
526
+ noExternal: []
527
+
528
+ }
529
+ };
530
+
531
+ // don't warn on overriding defaults
532
+ const [modified_vite_config] = deep_merge(default_config, vite_config);
533
+
534
+ /** @type {[any, string[]]} */
535
+ const [merged_config, conflicts] = deep_merge(
536
+ modified_vite_config,
537
+ get_default_config({ ...options, input, ssr: true })
538
+ );
539
+
540
+ print_config_conflicts(conflicts, 'kit.vite.', 'build_server');
541
+
542
+ process.env.VITE_SVELTEKIT_ADAPTER_NAME = config.kit.adapter?.name;
543
+
544
+ const { chunks } = await create_build(merged_config);
545
+
546
+ /** @type {import('vite').Manifest} */
547
+ const vite_manifest = JSON.parse(fs__default.readFileSync(`${output_dir}/server/manifest.json`, 'utf-8'));
548
+
549
+ mkdirp(`${output_dir}/server/nodes`);
550
+ mkdirp(`${output_dir}/server/stylesheets`);
551
+
552
+ const stylesheet_lookup = new Map();
553
+
554
+ client.assets.forEach((asset) => {
555
+ if (asset.fileName.endsWith('.css')) {
556
+ if (asset.source.length < config.kit.inlineStyleThreshold) {
557
+ const index = stylesheet_lookup.size;
558
+ const file = `${output_dir}/server/stylesheets/${index}.js`;
559
+
560
+ fs__default.writeFileSync(file, `// ${asset.fileName}\nexport default ${s(asset.source)};`);
561
+ stylesheet_lookup.set(asset.fileName, index);
169
562
  }
563
+ }
564
+ });
565
+
566
+ manifest_data.components.forEach((component, i) => {
567
+ const file = `${output_dir}/server/nodes/${i}.js`;
170
568
 
171
- /** @param {Error} error */
172
- function fix_stack_trace(error) {
173
- return error.stack ? vite.ssrRewriteStacktrace(error.stack) : error.stack;
569
+ const js = new Set();
570
+ const css = new Set();
571
+ find_deps(component, client.vite_manifest, js, css);
572
+
573
+ const imports = [`import * as module from '../${vite_manifest[component].file}';`];
574
+
575
+ const exports = [
576
+ 'export { module };',
577
+ `export const index = ${i};`,
578
+ `export const entry = '${client.vite_manifest[component].file}';`,
579
+ `export const js = ${s(Array.from(js))};`,
580
+ `export const css = ${s(Array.from(css))};`
581
+ ];
582
+
583
+ /** @type {string[]} */
584
+ const styles = [];
585
+
586
+ css.forEach((file) => {
587
+ if (stylesheet_lookup.has(file)) {
588
+ const index = stylesheet_lookup.get(file);
589
+ const name = `stylesheet_${index}`;
590
+ imports.push(`import ${name} from '../stylesheets/${index}.js';`);
591
+ styles.push(`\t${s(file)}: ${name}`);
174
592
  }
593
+ });
594
+
595
+ if (styles.length > 0) {
596
+ exports.push(`export const styles = {\n${styles.join(',\n')}\n};`);
597
+ }
598
+
599
+ fs__default.writeFileSync(file, `${imports.join('\n')}\n\n${exports.join('\n')}\n`);
600
+ });
601
+
602
+ return {
603
+ chunks,
604
+ vite_manifest,
605
+ methods: get_methods(cwd, chunks, manifest_data)
606
+ };
607
+ }
608
+
609
+ /** @type {Record<string, string>} */
610
+ const method_names = {
611
+ get: 'get',
612
+ head: 'head',
613
+ post: 'post',
614
+ put: 'put',
615
+ del: 'delete',
616
+ patch: 'patch'
617
+ };
618
+
619
+ /**
620
+ * @param {string} cwd
621
+ * @param {import('rollup').OutputChunk[]} output
622
+ * @param {import('types').ManifestData} manifest_data
623
+ */
624
+ function get_methods(cwd, output, manifest_data) {
625
+ /** @type {Record<string, string[]>} */
626
+ const lookup = {};
627
+ output.forEach((chunk) => {
628
+ if (!chunk.facadeModuleId) return;
629
+ const id = chunk.facadeModuleId.slice(cwd.length + 1);
630
+ lookup[id] = chunk.exports;
631
+ });
632
+
633
+ /** @type {Record<string, import('types').HttpMethod[]>} */
634
+ const methods = {};
635
+ manifest_data.routes.forEach((route) => {
636
+ const file = route.type === 'endpoint' ? route.file : route.shadow;
175
637
 
176
- update_manifest();
638
+ if (file && lookup[file]) {
639
+ methods[file] = lookup[file]
640
+ .map((x) => /** @type {import('types').HttpMethod} */ (method_names[x]))
641
+ .filter(Boolean);
642
+ }
643
+ });
644
+
645
+ return methods;
646
+ }
647
+
648
+ /** @typedef {{
649
+ * fn: () => Promise<any>,
650
+ * fulfil: (value: any) => void,
651
+ * reject: (error: Error) => void
652
+ * }} Task */
653
+
654
+ /** @param {number} concurrency */
655
+ function queue(concurrency) {
656
+ /** @type {Task[]} */
657
+ const tasks = [];
658
+
659
+ let current = 0;
177
660
 
178
- vite.watcher.on('add', update_manifest);
179
- vite.watcher.on('unlink', update_manifest);
661
+ /** @type {(value?: any) => void} */
662
+ let fulfil;
663
+
664
+ /** @type {(error: Error) => void} */
665
+ let reject;
666
+
667
+ let closed = false;
668
+
669
+ const done = new Promise((f, r) => {
670
+ fulfil = f;
671
+ reject = r;
672
+ });
673
+
674
+ done.catch(() => {
675
+ // this is necessary in case a catch handler is never added
676
+ // to the done promise by the user
677
+ });
180
678
 
181
- const assets = config.kit.paths.assets ? SVELTE_KIT_ASSETS : config.kit.paths.base;
182
- const asset_server = sirv(config.kit.files.assets, {
183
- dev: true,
184
- etag: true,
185
- maxAge: 0,
186
- extensions: []
679
+ function dequeue() {
680
+ if (current < concurrency) {
681
+ const task = tasks.shift();
682
+
683
+ if (task) {
684
+ current += 1;
685
+ const promise = Promise.resolve(task.fn());
686
+
687
+ promise
688
+ .then(task.fulfil, (err) => {
689
+ task.reject(err);
690
+ reject(err);
691
+ })
692
+ .then(() => {
693
+ current -= 1;
694
+ dequeue();
695
+ });
696
+ } else if (current === 0) {
697
+ closed = true;
698
+ fulfil();
699
+ }
700
+ }
701
+ }
702
+
703
+ return {
704
+ /** @param {() => any} fn */
705
+ add: (fn) => {
706
+ if (closed) throw new Error('Cannot add tasks to a queue that has ended');
707
+
708
+ const promise = new Promise((fulfil, reject) => {
709
+ tasks.push({ fn, fulfil, reject });
187
710
  });
188
711
 
189
- return () => {
190
- const serve_static_middleware = vite.middlewares.stack.find(
191
- (middleware) =>
192
- /** @type {function} */ (middleware.handle).name === 'viteServeStaticMiddleware'
193
- );
712
+ dequeue();
713
+ return promise;
714
+ },
194
715
 
195
- remove_html_middlewares(vite.middlewares);
716
+ done: () => {
717
+ if (current === 0) {
718
+ closed = true;
719
+ fulfil();
720
+ }
196
721
 
197
- vite.middlewares.use(async (req, res) => {
198
- try {
199
- if (!req.url || !req.method) throw new Error('Incomplete request');
722
+ return done;
723
+ }
724
+ };
725
+ }
200
726
 
201
- const base = `${vite.config.server.https ? 'https' : 'http'}://${
202
- req.headers[':authority'] || req.headers.host
203
- }`;
727
+ const DOCTYPE = 'DOCTYPE';
728
+ const CDATA_OPEN = '[CDATA[';
729
+ const CDATA_CLOSE = ']]>';
730
+ const COMMENT_OPEN = '--';
731
+ const COMMENT_CLOSE = '-->';
204
732
 
205
- const decoded = decodeURI(new URL(base + req.url).pathname);
733
+ const TAG_OPEN = /[a-zA-Z]/;
734
+ const TAG_CHAR = /[a-zA-Z0-9]/;
735
+ const ATTRIBUTE_NAME = /[^\t\n\f />"'=]/;
206
736
 
207
- if (decoded.startsWith(assets)) {
208
- const pathname = decoded.slice(assets.length);
209
- const file = config.kit.files.assets + pathname;
737
+ const WHITESPACE = /[\s\n\r]/;
210
738
 
211
- if (fs__default.existsSync(file) && !fs__default.statSync(file).isDirectory()) {
212
- const has_correct_case = fs__default.realpathSync.native(file) === path__default.resolve(file);
739
+ /** @param {string} html */
740
+ function crawl(html) {
741
+ /** @type {string[]} */
742
+ const hrefs = [];
213
743
 
214
- if (has_correct_case) {
215
- req.url = encodeURI(pathname); // don't need query/hash
216
- asset_server(req, res);
217
- return;
218
- }
219
- }
744
+ let i = 0;
745
+ main: while (i < html.length) {
746
+ const char = html[i];
747
+
748
+ if (char === '<') {
749
+ if (html[i + 1] === '!') {
750
+ i += 2;
751
+
752
+ if (html.slice(i, i + DOCTYPE.length).toUpperCase() === DOCTYPE) {
753
+ i += DOCTYPE.length;
754
+ while (i < html.length) {
755
+ if (html[i++] === '>') {
756
+ continue main;
220
757
  }
758
+ }
759
+ }
221
760
 
222
- if (!decoded.startsWith(config.kit.paths.base)) {
223
- return not_found(res, `Not found (did you mean ${config.kit.paths.base + req.url}?)`);
761
+ // skip cdata
762
+ if (html.slice(i, i + CDATA_OPEN.length) === CDATA_OPEN) {
763
+ i += CDATA_OPEN.length;
764
+ while (i < html.length) {
765
+ if (html.slice(i, i + CDATA_CLOSE.length) === CDATA_CLOSE) {
766
+ i += CDATA_CLOSE.length;
767
+ continue main;
224
768
  }
225
769
 
226
- /** @type {Partial<import('types').Hooks>} */
227
- const user_hooks = resolve_entry(config.kit.files.hooks)
228
- ? await vite.ssrLoadModule(`/${config.kit.files.hooks}`, { fixStacktrace: false })
229
- : {};
230
-
231
- const handle = user_hooks.handle || (({ event, resolve }) => resolve(event));
232
-
233
- /** @type {import('types').Hooks} */
234
- const hooks = {
235
- getSession: user_hooks.getSession || (() => ({})),
236
- handle,
237
- handleError:
238
- user_hooks.handleError ||
239
- (({ /** @type {Error & { frame?: string }} */ error }) => {
240
- console.error($.bold().red(error.message));
241
- if (error.frame) {
242
- console.error($.gray(error.frame));
243
- }
244
- if (error.stack) {
245
- console.error($.gray(error.stack));
246
- }
247
- }),
248
- externalFetch: user_hooks.externalFetch || fetch
249
- };
250
-
251
- if (/** @type {any} */ (hooks).getContext) {
252
- // TODO remove this for 1.0
253
- throw new Error(
254
- 'The getContext hook has been removed. See https://kit.svelte.dev/docs/hooks'
255
- );
770
+ i += 1;
771
+ }
772
+ }
773
+
774
+ // skip comments
775
+ if (html.slice(i, i + COMMENT_OPEN.length) === COMMENT_OPEN) {
776
+ i += COMMENT_OPEN.length;
777
+ while (i < html.length) {
778
+ if (html.slice(i, i + COMMENT_CLOSE.length) === COMMENT_CLOSE) {
779
+ i += COMMENT_CLOSE.length;
780
+ continue main;
256
781
  }
257
782
 
258
- if (/** @type {any} */ (hooks).serverFetch) {
259
- // TODO remove this for 1.0
260
- throw new Error('The serverFetch hook has been renamed to externalFetch.');
783
+ i += 1;
784
+ }
785
+ }
786
+ }
787
+
788
+ // parse opening tags
789
+ const start = ++i;
790
+ if (TAG_OPEN.test(html[start])) {
791
+ while (i < html.length) {
792
+ if (!TAG_CHAR.test(html[i])) {
793
+ break;
794
+ }
795
+
796
+ i += 1;
797
+ }
798
+
799
+ const tag = html.slice(start, i).toUpperCase();
800
+
801
+ if (tag === 'SCRIPT' || tag === 'STYLE') {
802
+ while (i < html.length) {
803
+ if (
804
+ html[i] === '<' &&
805
+ html[i + 1] === '/' &&
806
+ html.slice(i + 2, i + 2 + tag.length).toUpperCase() === tag
807
+ ) {
808
+ continue main;
261
809
  }
262
810
 
263
- // TODO the / prefix will probably fail if outDir is outside the cwd (which
264
- // could be the case in a monorepo setup), but without it these modules
265
- // can get loaded twice via different URLs, which causes failures. Might
266
- // require changes to Vite to fix
267
- const { default: root } = await vite.ssrLoadModule(
268
- `/${posixify(path__default.relative(cwd$1, `${config.kit.outDir}/generated/root.svelte`))}`,
269
- { fixStacktrace: false }
270
- );
271
-
272
- const paths = await vite.ssrLoadModule(
273
- true
274
- ? `/${posixify(path__default.relative(cwd$1, `${config.kit.outDir}/runtime/paths.js`))}`
275
- : `/@fs${runtime}/paths.js`,
276
- { fixStacktrace: false }
277
- );
278
-
279
- paths.set_paths({
280
- base: config.kit.paths.base,
281
- assets
282
- });
811
+ i += 1;
812
+ }
813
+ }
814
+
815
+ let href = '';
816
+ let rel = '';
817
+
818
+ while (i < html.length) {
819
+ const start = i;
283
820
 
284
- let request;
821
+ const char = html[start];
822
+ if (char === '>') break;
285
823
 
286
- try {
287
- request = await getRequest(base, req);
288
- } catch (/** @type {any} */ err) {
289
- res.statusCode = err.status || 400;
290
- return res.end(err.reason || 'Invalid request body');
824
+ if (ATTRIBUTE_NAME.test(char)) {
825
+ i += 1;
826
+
827
+ while (i < html.length) {
828
+ if (!ATTRIBUTE_NAME.test(html[i])) {
829
+ break;
830
+ }
831
+
832
+ i += 1;
291
833
  }
292
834
 
293
- const template = load_template(cwd$1, config);
294
-
295
- const rendered = await respond(
296
- request,
297
- {
298
- csp: config.kit.csp,
299
- dev: true,
300
- floc: config.kit.floc,
301
- get_stack: (error) => {
302
- return fix_stack_trace(error);
303
- },
304
- handle_error: (error, event) => {
305
- hooks.handleError({
306
- error: new Proxy(error, {
307
- get: (target, property) => {
308
- if (property === 'stack') {
309
- return fix_stack_trace(error);
310
- }
311
-
312
- return Reflect.get(target, property, target);
313
- }
314
- }),
315
- event,
316
-
317
- // TODO remove for 1.0
318
- // @ts-expect-error
319
- get request() {
320
- throw new Error(
321
- 'request in handleError has been replaced with event. See https://github.com/sveltejs/kit/pull/3384 for details'
322
- );
835
+ const name = html.slice(start, i).toLowerCase();
836
+
837
+ while (WHITESPACE.test(html[i])) i += 1;
838
+
839
+ if (html[i] === '=') {
840
+ i += 1;
841
+ while (WHITESPACE.test(html[i])) i += 1;
842
+
843
+ let value;
844
+
845
+ if (html[i] === "'" || html[i] === '"') {
846
+ const quote = html[i++];
847
+
848
+ const start = i;
849
+ let escaped = false;
850
+
851
+ while (i < html.length) {
852
+ if (!escaped) {
853
+ const char = html[i];
854
+
855
+ if (html[i] === quote) {
856
+ break;
857
+ }
858
+
859
+ if (char === '\\') {
860
+ escaped = true;
323
861
  }
324
- });
325
- },
326
- hooks,
327
- hydrate: config.kit.browser.hydrate,
328
- manifest,
329
- method_override: config.kit.methodOverride,
330
- paths: {
331
- base: config.kit.paths.base,
332
- assets
333
- },
334
- prefix: '',
335
- prerender: {
336
- default: config.kit.prerender.default,
337
- enabled: config.kit.prerender.enabled
338
- },
339
- read: (file) => fs__default.readFileSync(path__default.join(config.kit.files.assets, file)),
340
- root,
341
- router: config.kit.browser.router,
342
- template: ({ head, body, assets, nonce }) => {
343
- return (
344
- template
345
- .replace(/%sveltekit\.assets%/g, assets)
346
- .replace(/%sveltekit\.nonce%/g, nonce)
347
- // head and body must be replaced last, in case someone tries to sneak in %sveltekit.assets% etc
348
- .replace('%sveltekit.head%', () => head)
349
- .replace('%sveltekit.body%', () => body)
350
- );
351
- },
352
- template_contains_nonce: template.includes('%sveltekit.nonce%'),
353
- trailing_slash: config.kit.trailingSlash
354
- },
355
- {
356
- getClientAddress: () => {
357
- const { remoteAddress } = req.socket;
358
- if (remoteAddress) return remoteAddress;
359
- throw new Error('Could not determine clientAddress');
862
+ }
863
+
864
+ i += 1;
360
865
  }
866
+
867
+ value = html.slice(start, i);
868
+ } else {
869
+ const start = i;
870
+ while (html[i] !== '>' && !WHITESPACE.test(html[i])) i += 1;
871
+ value = html.slice(start, i);
872
+
873
+ i -= 1;
361
874
  }
362
- );
363
875
 
364
- if (rendered.status === 404) {
365
- // @ts-expect-error
366
- serve_static_middleware.handle(req, res, () => {
367
- setResponse(res, rendered);
368
- });
876
+ if (name === 'href') {
877
+ href = value;
878
+ } else if (name === 'rel') {
879
+ rel = value;
880
+ } else if (name === 'src') {
881
+ hrefs.push(value);
882
+ } else if (name === 'srcset') {
883
+ const candidates = [];
884
+ let insideURL = true;
885
+ value = value.trim();
886
+ for (let i = 0; i < value.length; i++) {
887
+ if (value[i] === ',' && (!insideURL || (insideURL && value[i + 1] === ' '))) {
888
+ candidates.push(value.slice(0, i));
889
+ value = value.substring(i + 1).trim();
890
+ i = 0;
891
+ insideURL = true;
892
+ } else if (value[i] === ' ') {
893
+ insideURL = false;
894
+ }
895
+ }
896
+ candidates.push(value);
897
+ for (const candidate of candidates) {
898
+ const src = candidate.split(WHITESPACE)[0];
899
+ hrefs.push(src);
900
+ }
901
+ }
369
902
  } else {
370
- setResponse(res, rendered);
903
+ i -= 1;
371
904
  }
372
- } catch (e) {
373
- const error = coalesce_to_error(e);
374
- vite.ssrFixStacktrace(error);
375
- res.statusCode = 500;
376
- res.end(error.stack);
377
905
  }
378
- });
379
- };
906
+
907
+ i += 1;
908
+ }
909
+
910
+ if (href && !/\bexternal\b/i.test(rel)) {
911
+ hrefs.push(href);
912
+ }
913
+ }
380
914
  }
381
- };
382
- }
383
915
 
384
- /** @param {import('http').ServerResponse} res */
385
- function not_found(res, message = 'Not found') {
386
- res.statusCode = 404;
387
- res.end(message);
916
+ i += 1;
917
+ }
918
+
919
+ return hrefs;
388
920
  }
389
921
 
390
922
  /**
391
- * @param {import('connect').Server} server
923
+ * Inside a script element, only `</script` and `<!--` hold special meaning to the HTML parser.
924
+ *
925
+ * The first closes the script element, so everything after is treated as raw HTML.
926
+ * The second disables further parsing until `-->`, so the script element might be unexpectedly
927
+ * kept open until until an unrelated HTML comment in the page.
928
+ *
929
+ * U+2028 LINE SEPARATOR and U+2029 PARAGRAPH SEPARATOR are escaped for the sake of pre-2018
930
+ * browsers.
931
+ *
932
+ * @see tests for unsafe parsing examples.
933
+ * @see https://html.spec.whatwg.org/multipage/scripting.html#restrictions-for-contents-of-script-elements
934
+ * @see https://html.spec.whatwg.org/multipage/syntax.html#cdata-rcdata-restrictions
935
+ * @see https://html.spec.whatwg.org/multipage/parsing.html#script-data-state
936
+ * @see https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escaped-state
937
+ * @see https://github.com/tc39/proposal-json-superset
938
+ * @type {Record<string, string>}
939
+ */
940
+ const render_json_payload_script_dict = {
941
+ '<': '\\u003C',
942
+ '\u2028': '\\u2028',
943
+ '\u2029': '\\u2029'
944
+ };
945
+
946
+ new RegExp(
947
+ `[${Object.keys(render_json_payload_script_dict).join('')}]`,
948
+ 'g'
949
+ );
950
+
951
+ /**
952
+ * When inside a double-quoted attribute value, only `&` and `"` hold special meaning.
953
+ * @see https://html.spec.whatwg.org/multipage/parsing.html#attribute-value-(double-quoted)-state
954
+ * @type {Record<string, string>}
955
+ */
956
+ const escape_html_attr_dict = {
957
+ '&': '&amp;',
958
+ '"': '&quot;'
959
+ };
960
+
961
+ const escape_html_attr_regex = new RegExp(
962
+ // special characters
963
+ `[${Object.keys(escape_html_attr_dict).join('')}]|` +
964
+ // high surrogate without paired low surrogate
965
+ '[\\ud800-\\udbff](?![\\udc00-\\udfff])|' +
966
+ // a valid surrogate pair, the only match with 2 code units
967
+ // we match it so that we can match unpaired low surrogates in the same pass
968
+ // TODO: use lookbehind assertions once they are widely supported: (?<![\ud800-udbff])[\udc00-\udfff]
969
+ '[\\ud800-\\udbff][\\udc00-\\udfff]|' +
970
+ // unpaired low surrogate (see previous match)
971
+ '[\\udc00-\\udfff]',
972
+ 'g'
973
+ );
974
+
975
+ /**
976
+ * Formats a string to be used as an attribute's value in raw HTML.
977
+ *
978
+ * It escapes unpaired surrogates (which are allowed in js strings but invalid in HTML), escapes
979
+ * characters that are special in attributes, and surrounds the whole string in double-quotes.
980
+ *
981
+ * @param {string} str
982
+ * @returns {string} Escaped string surrounded by double-quotes.
983
+ * @example const html = `<tag data-value=${escape_html_attr('value')}>...</tag>`;
392
984
  */
393
- function remove_html_middlewares(server) {
394
- const html_middlewares = [
395
- 'viteIndexHtmlMiddleware',
396
- 'vite404Middleware',
397
- 'viteSpaFallbackMiddleware',
398
- 'viteServeStaticMiddleware'
399
- ];
400
- for (let i = server.stack.length - 1; i > 0; i--) {
401
- // @ts-expect-error using internals until https://github.com/vitejs/vite/pull/4640 is merged
402
- if (html_middlewares.includes(server.stack[i].handle.name)) {
403
- server.stack.splice(i, 1);
985
+ function escape_html_attr(str) {
986
+ const escaped_str = str.replace(escape_html_attr_regex, (match) => {
987
+ if (match.length === 2) {
988
+ // valid surrogate pair
989
+ return match;
404
990
  }
991
+
992
+ return escape_html_attr_dict[match] ?? `&#${match.charCodeAt(0)};`;
993
+ });
994
+
995
+ return `"${escaped_str}"`;
996
+ }
997
+
998
+ /**
999
+ * @typedef {import('types').PrerenderErrorHandler} PrerenderErrorHandler
1000
+ * @typedef {import('types').PrerenderOnErrorValue} OnError
1001
+ * @typedef {import('types').Logger} Logger
1002
+ */
1003
+
1004
+ /** @type {(details: Parameters<PrerenderErrorHandler>[0] ) => string} */
1005
+ function format_error({ status, path, referrer, referenceType }) {
1006
+ return `${status} ${path}${referrer ? ` (${referenceType} from ${referrer})` : ''}`;
1007
+ }
1008
+
1009
+ /** @type {(log: Logger, onError: OnError) => PrerenderErrorHandler} */
1010
+ function normalise_error_handler(log, onError) {
1011
+ switch (onError) {
1012
+ case 'continue':
1013
+ return (details) => {
1014
+ log.error(format_error(details));
1015
+ };
1016
+ case 'fail':
1017
+ return (details) => {
1018
+ throw new Error(format_error(details));
1019
+ };
1020
+ default:
1021
+ return onError;
405
1022
  }
406
1023
  }
407
1024
 
1025
+ const OK = 2;
1026
+ const REDIRECT = 3;
1027
+
408
1028
  /**
409
- * @param {import('vite').ViteDevServer} vite
410
- * @param {import('vite').ModuleNode} node
411
- * @param {Set<import('vite').ModuleNode>} deps
1029
+ * @param {{
1030
+ * config: import('types').ValidatedConfig;
1031
+ * entries: string[];
1032
+ * files: Set<string>;
1033
+ * log: Logger;
1034
+ * }} opts
412
1035
  */
413
- async function find_deps(vite, node, deps) {
414
- // since `ssrTransformResult.deps` contains URLs instead of `ModuleNode`s, this process is asynchronous.
415
- // instead of using `await`, we resolve all branches in parallel.
416
- /** @type {Promise<void>[]} */
417
- const branches = [];
418
-
419
- /** @param {import('vite').ModuleNode} node */
420
- async function add(node) {
421
- if (!deps.has(node)) {
422
- deps.add(node);
423
- await find_deps(vite, node, deps);
424
- }
1036
+ async function prerender({ config, entries, files, log }) {
1037
+ /** @type {import('types').Prerendered} */
1038
+ const prerendered = {
1039
+ pages: new Map(),
1040
+ assets: new Map(),
1041
+ redirects: new Map(),
1042
+ paths: []
1043
+ };
1044
+
1045
+ if (!config.kit.prerender.enabled) {
1046
+ return prerendered;
425
1047
  }
426
1048
 
427
- /** @param {string} url */
428
- async function add_by_url(url) {
429
- const node = await vite.moduleGraph.getModuleByUrl(url);
1049
+ installPolyfills();
1050
+
1051
+ const server_root = join(config.kit.outDir, 'output');
1052
+
1053
+ /** @type {import('types').ServerModule} */
1054
+ const { Server, override } = await import(pathToFileURL(`${server_root}/server/index.js`).href);
1055
+ const { manifest } = await import(pathToFileURL(`${server_root}/server/manifest.js`).href);
1056
+
1057
+ override({
1058
+ paths: config.kit.paths,
1059
+ prerendering: true,
1060
+ read: (file) => readFileSync(join(config.kit.files.assets, file))
1061
+ });
1062
+
1063
+ const server = new Server(manifest);
1064
+
1065
+ const error = normalise_error_handler(log, config.kit.prerender.onError);
1066
+
1067
+ const q = queue(config.kit.prerender.concurrency);
430
1068
 
431
- if (node) {
432
- await add(node);
1069
+ /**
1070
+ * @param {string} path
1071
+ * @param {boolean} is_html
1072
+ */
1073
+ function output_filename(path, is_html) {
1074
+ const file = path.slice(config.kit.paths.base.length + 1);
1075
+
1076
+ if (file === '') {
1077
+ return 'index.html';
1078
+ }
1079
+
1080
+ if (is_html && !file.endsWith('.html')) {
1081
+ return file + (file.endsWith('/') ? 'index.html' : '.html');
433
1082
  }
1083
+
1084
+ return file;
1085
+ }
1086
+
1087
+ const seen = new Set();
1088
+ const written = new Set();
1089
+
1090
+ /**
1091
+ * @param {string | null} referrer
1092
+ * @param {string} decoded
1093
+ * @param {string} [encoded]
1094
+ */
1095
+ function enqueue(referrer, decoded, encoded) {
1096
+ if (seen.has(decoded)) return;
1097
+ seen.add(decoded);
1098
+
1099
+ const file = decoded.slice(config.kit.paths.base.length + 1);
1100
+ if (files.has(file)) return;
1101
+
1102
+ return q.add(() => visit(decoded, encoded || encodeURI(decoded), referrer));
434
1103
  }
435
1104
 
436
- if (node.ssrTransformResult) {
437
- if (node.ssrTransformResult.deps) {
438
- node.ssrTransformResult.deps.forEach((url) => branches.push(add_by_url(url)));
1105
+ /**
1106
+ * @param {string} decoded
1107
+ * @param {string} encoded
1108
+ * @param {string?} referrer
1109
+ */
1110
+ async function visit(decoded, encoded, referrer) {
1111
+ if (!decoded.startsWith(config.kit.paths.base)) {
1112
+ error({ status: 404, path: decoded, referrer, referenceType: 'linked' });
1113
+ return;
1114
+ }
1115
+
1116
+ /** @type {Map<string, import('types').PrerenderDependency>} */
1117
+ const dependencies = new Map();
1118
+
1119
+ const response = await server.respond(new Request(`http://sveltekit-prerender${encoded}`), {
1120
+ getClientAddress,
1121
+ prerendering: {
1122
+ dependencies
1123
+ }
1124
+ });
1125
+
1126
+ const text = await response.text();
1127
+
1128
+ save('pages', response, text, decoded, encoded, referrer, 'linked');
1129
+
1130
+ for (const [dependency_path, result] of dependencies) {
1131
+ // this seems circuitous, but using new URL allows us to not care
1132
+ // whether dependency_path is encoded or not
1133
+ const encoded_dependency_path = new URL$1(dependency_path, 'http://localhost').pathname;
1134
+ const decoded_dependency_path = decodeURI(encoded_dependency_path);
1135
+
1136
+ const body = result.body ?? new Uint8Array(await result.response.arrayBuffer());
1137
+ save(
1138
+ 'dependencies',
1139
+ result.response,
1140
+ body,
1141
+ decoded_dependency_path,
1142
+ encoded_dependency_path,
1143
+ decoded,
1144
+ 'fetched'
1145
+ );
1146
+ }
1147
+
1148
+ if (config.kit.prerender.crawl && response.headers.get('content-type') === 'text/html') {
1149
+ for (const href of crawl(text)) {
1150
+ if (href.startsWith('data:') || href.startsWith('#')) continue;
1151
+
1152
+ const resolved = resolve(encoded, href);
1153
+ if (!is_root_relative(resolved)) continue;
1154
+
1155
+ const { pathname, search } = new URL$1(resolved, 'http://localhost');
1156
+
1157
+ enqueue(decoded, decodeURI(pathname), pathname);
1158
+ }
439
1159
  }
440
- } else {
441
- node.importedModules.forEach((node) => branches.push(add(node)));
442
1160
  }
443
1161
 
444
- await Promise.all(branches);
445
- }
1162
+ /**
1163
+ * @param {'pages' | 'dependencies'} category
1164
+ * @param {Response} response
1165
+ * @param {string | Uint8Array} body
1166
+ * @param {string} decoded
1167
+ * @param {string} encoded
1168
+ * @param {string | null} referrer
1169
+ * @param {'linked' | 'fetched'} referenceType
1170
+ */
1171
+ function save(category, response, body, decoded, encoded, referrer, referenceType) {
1172
+ const response_type = Math.floor(response.status / 100);
1173
+ const type = /** @type {string} */ (response.headers.get('content-type'));
1174
+ const is_html = response_type === REDIRECT || type === 'text/html';
1175
+
1176
+ const file = output_filename(decoded, is_html);
1177
+ const dest = `${config.kit.outDir}/output/prerendered/${category}/${file}`;
1178
+
1179
+ if (written.has(file)) return;
1180
+
1181
+ if (response_type === REDIRECT) {
1182
+ const location = response.headers.get('location');
1183
+
1184
+ if (location) {
1185
+ const resolved = resolve(encoded, location);
1186
+ if (is_root_relative(resolved)) {
1187
+ enqueue(decoded, decodeURI(resolved), resolved);
1188
+ }
446
1189
 
447
- const cwd = process.cwd();
1190
+ if (!response.headers.get('x-sveltekit-normalize')) {
1191
+ mkdirp(dirname(dest));
448
1192
 
449
- /**
450
- * @typedef {{
451
- * port: number,
452
- * host?: string,
453
- * https: boolean,
454
- * }} Options
455
- * @typedef {import('types').SSRComponent} SSRComponent
456
- */
1193
+ log.warn(`${response.status} ${decoded} -> ${location}`);
457
1194
 
458
- /** @param {Options} opts */
459
- async function dev({ port, host, https }) {
460
- /** @type {import('types').ValidatedConfig} */
461
- const config = await load_config();
462
-
463
- init(config);
464
-
465
- const [vite_config] = deep_merge(
466
- {
467
- server: {
468
- fs: {
469
- allow: [
470
- ...new Set([
471
- config.kit.files.lib,
472
- config.kit.files.routes,
473
- config.kit.outDir,
474
- path__default.resolve(cwd, 'src'),
475
- path__default.resolve(cwd, 'node_modules'),
476
- path__default.resolve(vite.searchForWorkspaceRoot(cwd), 'node_modules')
477
- ])
478
- ]
479
- },
480
- port: 3000,
481
- strictPort: true,
482
- watch: {
483
- ignored: [`${config.kit.outDir}/**`, `!${config.kit.outDir}/generated/**`]
1195
+ writeFileSync(
1196
+ dest,
1197
+ `<meta http-equiv="refresh" content=${escape_html_attr(`0;url=${location}`)}>`
1198
+ );
1199
+
1200
+ written.add(file);
1201
+
1202
+ if (!prerendered.redirects.has(decoded)) {
1203
+ prerendered.redirects.set(decoded, {
1204
+ status: response.status,
1205
+ location: resolved
1206
+ });
1207
+
1208
+ prerendered.paths.push(normalize_path(decoded, 'never'));
1209
+ }
484
1210
  }
1211
+ } else {
1212
+ log.warn(`location header missing on redirect received from ${decoded}`);
485
1213
  }
486
- },
487
- await config.kit.vite()
488
- );
489
1214
 
490
- /** @type {[any, string[]]} */
491
- const [merged_config, conflicts] = deep_merge(vite_config, {
492
- configFile: false,
493
- root: cwd,
494
- resolve: {
495
- alias: get_aliases(config)
496
- },
497
- build: {
498
- rollupOptions: {
499
- // Vite dependency crawler needs an explicit JS entry point
500
- // eventhough server otherwise works without it
501
- input: `${get_runtime_path(config)}/client/start.js`
502
- }
503
- },
504
- plugins: [
505
- svelte({
506
- ...config,
507
- emitCss: true,
508
- compilerOptions: {
509
- ...config.compilerOptions,
510
- hydratable: !!config.kit.browser.hydrate
511
- },
512
- configFile: false
513
- }),
514
- await create_plugin(config)
515
- ],
516
- base: '/'
517
- });
1215
+ return;
1216
+ }
518
1217
 
519
- print_config_conflicts(conflicts, 'kit.vite.');
1218
+ if (response.status === 200) {
1219
+ mkdirp(dirname(dest));
520
1220
 
521
- // optional config from command-line flags
522
- // these should take precedence, but not print conflict warnings
523
- if (host) {
524
- merged_config.server.host = host;
525
- }
1221
+ log.info(`${response.status} ${decoded}`);
1222
+ writeFileSync(dest, body);
1223
+ written.add(file);
1224
+
1225
+ if (is_html) {
1226
+ prerendered.pages.set(decoded, {
1227
+ file
1228
+ });
1229
+ } else {
1230
+ prerendered.assets.set(decoded, {
1231
+ type
1232
+ });
1233
+ }
526
1234
 
527
- // if https is already enabled then do nothing. it could be an object and we
528
- // don't want to overwrite with a boolean
529
- if (https && !merged_config.server.https) {
530
- merged_config.server.https = https;
1235
+ prerendered.paths.push(normalize_path(decoded, 'never'));
1236
+ } else if (response_type !== OK) {
1237
+ error({ status: response.status, path: decoded, referrer, referenceType });
1238
+ }
531
1239
  }
532
1240
 
533
- if (port) {
534
- merged_config.server.port = port;
1241
+ if (config.kit.prerender.enabled) {
1242
+ for (const entry of config.kit.prerender.entries) {
1243
+ if (entry === '*') {
1244
+ for (const entry of entries) {
1245
+ enqueue(null, config.kit.paths.base + entry); // TODO can we pre-normalize these?
1246
+ }
1247
+ } else {
1248
+ enqueue(null, config.kit.paths.base + entry);
1249
+ }
1250
+ }
1251
+
1252
+ await q.done();
535
1253
  }
536
1254
 
537
- const server = await vite.createServer(merged_config);
538
- await server.listen(port);
1255
+ const rendered = await server.respond(new Request('http://sveltekit-prerender/[fallback]'), {
1256
+ getClientAddress,
1257
+ prerendering: {
1258
+ fallback: true,
1259
+ dependencies: new Map()
1260
+ }
1261
+ });
539
1262
 
540
- return {
541
- server,
542
- config
1263
+ const file = `${config.kit.outDir}/output/prerendered/fallback.html`;
1264
+ mkdirp(dirname(file));
1265
+ writeFileSync(file, await rendered.text());
1266
+
1267
+ return prerendered;
1268
+ }
1269
+
1270
+ /** @return {string} */
1271
+ function getClientAddress() {
1272
+ throw new Error('Cannot read clientAddress during prerendering');
1273
+ }
1274
+
1275
+ /**
1276
+ * @param {import('types').ValidatedConfig} config
1277
+ * @param {{ log: import('types').Logger }} opts
1278
+ */
1279
+ async function build(config, { log }) {
1280
+ const cwd = process.cwd(); // TODO is this necessary?
1281
+
1282
+ const build_dir = path__default.join(config.kit.outDir, 'build');
1283
+ rimraf(build_dir);
1284
+ mkdirp(build_dir);
1285
+
1286
+ const output_dir = path__default.join(config.kit.outDir, 'output');
1287
+ rimraf(output_dir);
1288
+ mkdirp(output_dir);
1289
+
1290
+ const { manifest_data } = all(config);
1291
+
1292
+ const options = {
1293
+ cwd,
1294
+ config,
1295
+ build_dir,
1296
+ manifest_data,
1297
+ output_dir,
1298
+ client_entry_file: path__default.relative(cwd, `${get_runtime_path(config)}/client/start.js`),
1299
+ service_worker_entry_file: resolve_entry(config.kit.files.serviceWorker)
1300
+ };
1301
+
1302
+ const client = await build_client(options);
1303
+ const server = await build_server(options, client);
1304
+
1305
+ /** @type {import('types').BuildData} */
1306
+ const build_data = {
1307
+ app_dir: config.kit.appDir,
1308
+ manifest_data: options.manifest_data,
1309
+ service_worker: options.service_worker_entry_file ? 'service-worker.js' : null, // TODO make file configurable?
1310
+ client,
1311
+ server
543
1312
  };
1313
+
1314
+ const manifest = `export const manifest = ${generate_manifest({
1315
+ build_data,
1316
+ relative_path: '.',
1317
+ routes: options.manifest_data.routes
1318
+ })};\n`;
1319
+ fs__default.writeFileSync(`${output_dir}/server/manifest.js`, manifest);
1320
+
1321
+ const static_files = options.manifest_data.assets.map((asset) => posixify(asset.file));
1322
+
1323
+ const files = new Set([
1324
+ ...static_files,
1325
+ ...client.chunks.map((chunk) => `${config.kit.appDir}/immutable/${chunk.fileName}`),
1326
+ ...client.assets.map((chunk) => `${config.kit.appDir}/immutable/${chunk.fileName}`)
1327
+ ]);
1328
+
1329
+ // TODO is this right?
1330
+ static_files.forEach((file) => {
1331
+ if (file.endsWith('/index.html')) {
1332
+ files.add(file.slice(0, -11));
1333
+ }
1334
+ });
1335
+
1336
+ const prerendered = await prerender({
1337
+ config,
1338
+ entries: options.manifest_data.routes
1339
+ .map((route) => (route.type === 'page' ? route.path : ''))
1340
+ .filter(Boolean),
1341
+ files,
1342
+ log
1343
+ });
1344
+
1345
+ if (options.service_worker_entry_file) {
1346
+ if (config.kit.paths.assets) {
1347
+ throw new Error('Cannot use service worker alongside config.kit.paths.assets');
1348
+ }
1349
+
1350
+ await build_service_worker(options, prerendered, client.vite_manifest);
1351
+ }
1352
+
1353
+ return { build_data, prerendered };
544
1354
  }
545
1355
 
546
- export { dev };
1356
+ export { build };