diff --git a/.changeset/fresh-shoes-count.md b/.changeset/fresh-shoes-count.md new file mode 100644 index 000000000000..b87a98b31a48 --- /dev/null +++ b/.changeset/fresh-shoes-count.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: reuse base64 and text decoding helpers diff --git a/packages/adapter-node/index.js b/packages/adapter-node/index.js index dd26779b9380..a85e3f8cef3a 100644 --- a/packages/adapter-node/index.js +++ b/packages/adapter-node/index.js @@ -7,6 +7,7 @@ const files = fileURLToPath(new URL('./files', import.meta.url).href); /** @param {string} str */ function escape_regex(str) { + // TODO replace with `RegExp.escape(str)` when we require Node >= 24 return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } diff --git a/packages/kit/src/runtime/client/utils.js b/packages/kit/src/runtime/client/utils.js index 23f9f6a750c3..f445bf2fb46c 100644 --- a/packages/kit/src/runtime/client/utils.js +++ b/packages/kit/src/runtime/client/utils.js @@ -122,6 +122,7 @@ export function get_link_info(a, base, uses_hash_router) { /** @type {URL | undefined} */ let url; + // TODO replace the try/catch with `URL.parse` when browser support allows (Chrome 126, Firefox 126, Safari 18) try { url = new URL(a instanceof SVGAElement ? a.href.baseVal : a.href, document.baseURI); diff --git a/packages/kit/src/runtime/form-utils.js b/packages/kit/src/runtime/form-utils.js index 1ba52fea0933..d8ddc1f3cb04 100644 --- a/packages/kit/src/runtime/form-utils.js +++ b/packages/kit/src/runtime/form-utils.js @@ -3,12 +3,10 @@ import { DEV } from 'esm-env'; import * as devalue from 'devalue'; -import { text_encoder } from './utils.js'; +import { text_decoder, text_encoder } from './utils.js'; import { noop } from '../utils/functions.js'; import { SvelteKitError } from '@sveltejs/kit/internal'; -const decoder = new TextDecoder(); - /** * Sets a parsed form field value in a nested object, mutating the original object. * @param {Record} object @@ -283,7 +281,7 @@ export async function deserialize_binary_form(request, form_id) { const file_offsets_buffer = await get_buffer(HEADER_BYTES + data_length, file_offsets_length); if (!file_offsets_buffer) throw deserialize_error('file offset table too short'); - const parsed_offsets = JSON.parse(decoder.decode(file_offsets_buffer)); + const parsed_offsets = JSON.parse(text_decoder.decode(file_offsets_buffer)); if ( !Array.isArray(parsed_offsets) || @@ -298,7 +296,7 @@ export async function deserialize_binary_form(request, form_id) { /** @type {Array<{ offset: number, size: number }>} */ const file_spans = []; - const [data, meta] = devalue.parse(decoder.decode(data_buffer), { + const [data, meta] = devalue.parse(text_decoder.decode(data_buffer), { File: ([name, type, size, last_modified, index]) => { if ( typeof name !== 'string' || @@ -488,7 +486,7 @@ class LazyFile { }); } async text() { - return decoder.decode(await this.arrayBuffer()); + return text_decoder.decode(await this.arrayBuffer()); } } diff --git a/packages/kit/src/runtime/server/page/crypto.js b/packages/kit/src/runtime/server/page/crypto.js index 1a08dd397e6b..f7a4ab71ae9b 100644 --- a/packages/kit/src/runtime/server/page/crypto.js +++ b/packages/kit/src/runtime/server/page/crypto.js @@ -1,4 +1,4 @@ -import { text_encoder } from '../../utils.js'; +import { base64_encode, text_encoder } from '../../utils.js'; /** * SHA-256 hashing function adapted from https://bitwiseshiftleft.github.io/sjcl @@ -102,7 +102,7 @@ export function sha256(data) { const bytes = new Uint8Array(out.buffer); reverse_endianness(bytes); - return btoa(String.fromCharCode(...bytes)); + return base64_encode(bytes); } /** The SHA-256 initialization vector */ diff --git a/packages/kit/src/runtime/server/page/csp.js b/packages/kit/src/runtime/server/page/csp.js index 47e1cea65d92..a85b62e845af 100644 --- a/packages/kit/src/runtime/server/page/csp.js +++ b/packages/kit/src/runtime/server/page/csp.js @@ -1,11 +1,12 @@ import { escape_html } from '../../../utils/escape.js'; +import { base64_encode } from '../../utils.js'; import { sha256 } from './crypto.js'; const array = new Uint8Array(16); function generate_nonce() { crypto.getRandomValues(array); - return btoa(String.fromCharCode(...array)); + return base64_encode(array); } const quoted = new Set([ diff --git a/packages/kit/src/runtime/server/page/load_data.js b/packages/kit/src/runtime/server/page/load_data.js index 67c9c3c37c07..07a58c7fa019 100644 --- a/packages/kit/src/runtime/server/page/load_data.js +++ b/packages/kit/src/runtime/server/page/load_data.js @@ -336,7 +336,7 @@ export function create_universal_fetch(event, state, fetched, csr, resolve_opts) const request_body = input instanceof Request && cloned_body - ? await stream_to_string(cloned_body) + ? await new Response(cloned_body).text() : init?.body; if ( @@ -371,16 +371,7 @@ export function create_universal_fetch(event, state, fetched, csr, resolve_opts) const [a, b] = response.body.tee(); void (async () => { - let result = new Uint8Array(); - - for await (const chunk of a) { - const combined = new Uint8Array(result.length + chunk.length); - - combined.set(result, 0); - combined.set(chunk, result.length); - - result = combined; - } + const result = new Uint8Array(await new Response(a).arrayBuffer()); if (dependency) { dependency.body = new Uint8Array(result); @@ -496,21 +487,3 @@ export function create_universal_fetch(event, state, fetched, csr, resolve_opts) return response; }; } - -/** - * @param {ReadableStream} stream - */ -async function stream_to_string(stream) { - let result = ''; - const reader = stream.getReader(); - const decoder = new TextDecoder(); - while (true) { - const { done, value } = await reader.read(); - if (done) { - result += decoder.decode(); - break; - } - result += decoder.decode(value, { stream: true }); - } - return result; -} diff --git a/packages/kit/src/runtime/shared.js b/packages/kit/src/runtime/shared.js index c3caa09a1fe7..2d56a0f9328c 100644 --- a/packages/kit/src/runtime/shared.js +++ b/packages/kit/src/runtime/shared.js @@ -1,6 +1,6 @@ /** @import { Transport } from '@sveltejs/kit' */ import * as devalue from 'devalue'; -import { base64_decode, base64_encode, text_encoder } from './utils.js'; +import { base64_decode, base64_encode, text_decoder, text_encoder } from './utils.js'; /** * @param {string} route_id @@ -331,6 +331,7 @@ export async function stringify_command_arg(value, transport) { */ function url_friendly_base64_encode(string) { const bytes = text_encoder.encode(string); + // TODO replace with `bytes.toBase64({ alphabet: 'base64url', omitPadding: true })` when we require Node >= 25 return base64_encode(bytes).replaceAll('=', '').replaceAll('+', '-').replaceAll('/', '_'); } @@ -342,7 +343,8 @@ function url_friendly_base64_encode(string) { export function parse_remote_arg(string, transport) { if (!string) return undefined; - const json_string = new TextDecoder().decode( + const json_string = text_decoder.decode( + // TODO replace with `Uint8Array.fromBase64(string, { alphabet: 'base64url' })` when we require Node >= 25 // no need to add back `=` characters, atob can handle it base64_decode(string.replaceAll('-', '+').replaceAll('_', '/')) ); diff --git a/packages/kit/src/runtime/utils.js b/packages/kit/src/runtime/utils.js index f19967160a9b..69e59e938bff 100644 --- a/packages/kit/src/runtime/utils.js +++ b/packages/kit/src/runtime/utils.js @@ -1,6 +1,7 @@ import { BROWSER } from 'esm-env'; export const text_encoder = new TextEncoder(); +export const text_decoder = new TextDecoder(); /** * Like node's path.relative, but without using node @@ -28,6 +29,7 @@ export function get_relative_path(from, to) { * @returns {string} */ export function base64_encode(bytes) { + // TODO replace with `bytes.toBase64()` when we require Node >= 25 // Using `Buffer` is faster than iterating if (!BROWSER && globalThis.Buffer) { return globalThis.Buffer.from(bytes).toString('base64'); @@ -47,6 +49,7 @@ export function base64_encode(bytes) { * @returns {Uint8Array} */ export function base64_decode(encoded) { + // TODO replace with `Uint8Array.fromBase64(encoded)` when we require Node >= 25 // Using `Buffer` is faster than iterating if (!BROWSER && globalThis.Buffer) { const buffer = globalThis.Buffer.from(encoded, 'base64'); diff --git a/packages/kit/src/utils/regex.js b/packages/kit/src/utils/regex.js index 79aa0ac82a73..df018a65fd43 100644 --- a/packages/kit/src/utils/regex.js +++ b/packages/kit/src/utils/regex.js @@ -4,5 +4,6 @@ * @returns {string} escaped string */ export function escape_for_regexp(str) { + // TODO replace with `RegExp.escape(str)` when we require Node >= 24 return str.replace(/[.*+?^${}()|[\]\\]/g, (match) => '\\' + match); } diff --git a/packages/kit/src/utils/url.js b/packages/kit/src/utils/url.js index 0e44ad3591aa..f9de4a596e6b 100644 --- a/packages/kit/src/utils/url.js +++ b/packages/kit/src/utils/url.js @@ -46,6 +46,7 @@ export function relative_pathname(from, to) { export function matches_external_allowlist_entry(location, allowed) { if (location === allowed) return true; + // TODO replace the try/catch with `URL.parse` when browser support allows (Chrome 126, Firefox 126, Safari 18) try { const allow = new URL(allowed); const loc = new URL(location, allow);