this repo has no description
at main 109 lines 3.7 kB view raw
1import { isPOJO } from '@amp/web-apps-utils'; 2 3// NOTE: be careful with imports here. This file is imported by browser code, 4// so we expect tree shaking to only keep these functions. 5 6const SERVER_DATA_ID = 'serialized-server-data'; 7 8// Take care with < (which has special meaning inside script tags) 9// See: https://github.com/sveltejs/kit/blob/ff9a27b4/packages/kit/src/runtime/server/page/serialize_data.js#L4-L28 10const replacements = { 11 '<': '\\u003C', 12 '\u2028': '\\u2028', 13 '\u2029': '\\u2029', 14}; 15 16const pattern = new RegExp(`[${Object.keys(replacements).join('')}]`, 'g'); 17 18/** 19 * Serializes a POJO into a HTML <script> tag that can be read clientside by 20 * `deserializeServerData`. 21 * 22 * Use this to share data between serverside and clientside. Include the 23 * returned HTML in the response to a client to allow it to read this data. 24 * 25 * @param data data to serialize 26 * @returns serialized data (or empty string if serialization fails) 27 */ 28export function serializeServerData(data: object): string { 29 try { 30 const sanitizedData = JSON.stringify(data).replace( 31 pattern, 32 (match) => replacements[match as keyof typeof replacements], 33 ); 34 return `<script type="application/json" id="${SERVER_DATA_ID}">${sanitizedData}</script>`; 35 } catch (e) { 36 // Don't let recursive data (or other non-serializable things) throw. 37 // We'd rather just let the serialize no-op to avoid breaking consumers. 38 return ''; 39 } 40} 41 42/** 43 * Deserializes data serialized on the server by `serializeServerData`. 44 * 45 * @returns deserialized data (or undefined if it doesn't exist/errors) 46 */ 47export function deserializeServerData(): ReturnType<JSON['parse']> | undefined { 48 const script = document.getElementById(SERVER_DATA_ID); 49 if (!script) { 50 return; 51 } 52 53 script.parentNode?.removeChild(script); 54 55 try { 56 return JSON.parse(script.textContent || ''); 57 } catch (e) { 58 // If the content is malformed, we want to avoid throwing. This 59 // situation should be impossible since we control the serialization 60 // above. 61 return; 62 } 63} 64 65/** 66 * JSON stringify a POJO value in a stable manner. Specifically, this means that 67 * objects which are structurally equal serialize to the same string. 68 * 69 * This is useful when comparing objects serialized by a server against objects 70 * build in browser. With plain JSON.stringify(), property order matters and is 71 * not guaranteed to be the same. In other words these two objects would 72 * JSON.stringify() differently: 73 * 74 * { a: 1, b: 2 } 75 * { b: 2, a: 1 } 76 * 77 * But these are structurally equal--they have the same keys and values. 78 * 79 * The expected use case for this function is generating keys for a Map for 80 * objects from a server that will be compared against objects from the browser. 81 * This function should be used on objects returned from `deserializeServerData` 82 * before they are used in such contexts. 83 * 84 * See: https://stackoverflow.com/a/43049877 85 */ 86export function stableStringify(data: unknown): string { 87 if (Array.isArray(data)) { 88 const items = data.map(stableStringify).join(','); 89 return `[${items}]`; 90 } 91 92 // Sort object keys before serializing 93 if (isPOJO(data)) { 94 const keys = [...Object.keys(data)]; 95 keys.sort(); 96 97 const properties = keys 98 // undefined values should not get included in stringification 99 .filter((key) => typeof data[key] !== 'undefined') 100 .map( 101 (key) => `${JSON.stringify(key)}:${stableStringify(data[key])}`, 102 ) 103 .join(','); 104 105 return `{${properties}}`; 106 } 107 108 return JSON.stringify(data); 109}