encodeBase64/decodeBase64 for UTF-8 text

TypeScript
const encoder = new TextEncoder();
const decoder = new TextDecoder();

/** Encode any text as Base64 over its UTF-8 bytes, not raw UTF-16 code units. */
export function encodeBase64(text: string): string {
  let binary = '';
  for (const byte of encoder.encode(text)) binary += String.fromCharCode(byte);
  return btoa(binary);
}

/** Decode Base64 text, rejecting characters or padding that cannot be valid. */
export function decodeBase64(encoded: string): string {
  const compact = encoded.replace(/\s+/g, '');
  if (!/^[A-Za-z0-9+/]*={0,2}$/.test(compact) || compact.length % 4 === 1) {
    throw new SyntaxError('input is not valid Base64');
  }
  const binary = atob(compact);
  const bytes = new Uint8Array(binary.length);
  for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
  return decoder.decode(bytes);
}

/** URL-safe Base64 without padding, for query params and file names. */
export function toBase64Url(text: string): string {
  return encodeBase64(text).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

// decodeBase64(encodeBase64('héllo 世界')) === 'héllo 世界'