diff --git a/.changeset/angry-tigers-prove.md b/.changeset/angry-tigers-prove.md new file mode 100644 index 000000000000..ba2ad1fd4150 --- /dev/null +++ b/.changeset/angry-tigers-prove.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +Follow redirects when prerendering diff --git a/packages/kit/src/core/adapt/prerender.js b/packages/kit/src/core/adapt/prerender.js index 00458476c342..97d47cb35142 100644 --- a/packages/kit/src/core/adapt/prerender.js +++ b/packages/kit/src/core/adapt/prerender.js @@ -1,10 +1,11 @@ import { readFileSync, writeFileSync } from 'fs'; import { dirname, join, resolve as resolve_path } from 'path'; -import { pathToFileURL, resolve, URL } from 'url'; +import { pathToFileURL, URL } from 'url'; import { mkdirp } from '../../utils/filesystem.js'; import { __fetch_polyfill } from '../../install-fetch.js'; import { SVELTE_KIT } from '../constants.js'; import { get_single_valued_header } from '../../utils/http.js'; +import { is_root_relative, resolve } from '../../utils/url.js'; /** * @typedef {import('types/config').PrerenderErrorHandler} PrerenderErrorHandler @@ -191,6 +192,11 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a log.warn(`${rendered.status} ${decoded_path} -> ${location}`); writeFileSync(file, ``); written_files.push(file); + + const resolved = resolve(path, location); + if (is_root_relative(resolved)) { + await visit(resolved, path); + } } else { log.warn(`location header missing on redirect received from ${decoded_path}`); } @@ -264,7 +270,7 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a if (!href) continue; const resolved = resolve(path, href); - if (!resolved.startsWith('/') || resolved.startsWith('//')) continue; + if (!is_root_relative(resolved)) continue; const parsed = new URL(resolved, 'http://localhost'); const pathname = decodeURI(parsed.pathname).replace(config.kit.paths.base, ''); diff --git a/packages/kit/src/runtime/server/page/load_node.js b/packages/kit/src/runtime/server/page/load_node.js index b6df74168d2f..9cb9b1b8eef9 100644 --- a/packages/kit/src/runtime/server/page/load_node.js +++ b/packages/kit/src/runtime/server/page/load_node.js @@ -1,6 +1,7 @@ import { normalize } from '../../load.js'; import { respond } from '../index.js'; import { escape_json_string_in_html } from '../../../utils/escape.js'; +import { is_root_relative, resolve } from '../../../utils/url.js'; const s = JSON.stringify; @@ -126,7 +127,7 @@ export async function load_node({ `http://${page.host}/${asset.file}`, /** @type {RequestInit} */ (opts) ); - } else if (resolved.startsWith('/') && !resolved.startsWith('//')) { + } else if (is_root_relative(resolved)) { const relative = resolved; const headers = /** @type {import('types/helper').RequestHeaders} */ ({ @@ -303,34 +304,3 @@ export async function load_node({ uses_credentials }; } - -const absolute = /^([a-z]+:)?\/?\//; - -/** - * @param {string} base - * @param {string} path - */ -export function resolve(base, path) { - const base_match = absolute.exec(base); - const path_match = absolute.exec(path); - - if (!base_match) { - throw new Error(`bad base path: "${base}"`); - } - - const baseparts = path_match ? [] : base.slice(base_match[0].length).split('/'); - const pathparts = path_match ? path.slice(path_match[0].length).split('/') : path.split('/'); - - baseparts.pop(); - - for (let i = 0; i < pathparts.length; i += 1) { - const part = pathparts[i]; - if (part === '.') continue; - else if (part === '..') baseparts.pop(); - else baseparts.push(part); - } - - const prefix = (path_match && path_match[0]) || (base_match && base_match[0]) || ''; - - return `${prefix}${baseparts.join('/')}`; -} diff --git a/packages/kit/src/utils/url.js b/packages/kit/src/utils/url.js new file mode 100644 index 000000000000..486742357d0c --- /dev/null +++ b/packages/kit/src/utils/url.js @@ -0,0 +1,35 @@ +const absolute = /^([a-z]+:)?\/?\//; + +/** + * @param {string} base + * @param {string} path + */ +export function resolve(base, path) { + const base_match = absolute.exec(base); + const path_match = absolute.exec(path); + + if (!base_match) { + throw new Error(`bad base path: "${base}"`); + } + + const baseparts = path_match ? [] : base.slice(base_match[0].length).split('/'); + const pathparts = path_match ? path.slice(path_match[0].length).split('/') : path.split('/'); + + baseparts.pop(); + + for (let i = 0; i < pathparts.length; i += 1) { + const part = pathparts[i]; + if (part === '.') continue; + else if (part === '..') baseparts.pop(); + else baseparts.push(part); + } + + const prefix = (path_match && path_match[0]) || (base_match && base_match[0]) || ''; + + return `${prefix}${baseparts.join('/')}`; +} + +/** @param {string} path */ +export function is_root_relative(path) { + return path[0] === '/' && path[1] !== '/'; +} diff --git a/packages/kit/src/runtime/server/page/load_node.spec.js b/packages/kit/src/utils/url.spec.js similarity index 97% rename from packages/kit/src/runtime/server/page/load_node.spec.js rename to packages/kit/src/utils/url.spec.js index 75fbd6f0845b..11df39fee009 100644 --- a/packages/kit/src/runtime/server/page/load_node.spec.js +++ b/packages/kit/src/utils/url.spec.js @@ -1,6 +1,6 @@ import { test } from 'uvu'; import * as assert from 'uvu/assert'; -import { resolve } from './load_node.js'; +import { resolve } from './url.js'; test('resolves a root-relative path', () => { assert.equal(resolve('/a/b/c', '/x/y/z'), '/x/y/z');