@sveltejs/kit 1.0.0-next.344 → 1.0.0-next.347

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