1/** 2 * Copyright © 2022 650 Industries. 3 * 4 * This source code is licensed under the MIT license found in the 5 * LICENSE file in the root directory of this source tree. 6 */ 7import { isJscSafeUrl, toNormalUrl } from 'jsc-safe-url'; 8import { MixedOutput } from 'metro'; 9import { InputConfigT, SerializerConfigT } from 'metro-config'; 10import baseJSBundle from 'metro/src/DeltaBundler/Serializers/baseJSBundle'; 11import bundleToString from 'metro/src/lib/bundleToString'; 12 13import { env } from '../env'; 14import { environmentVariableSerializerPlugin } from './environmentVariableSerializerPlugin'; 15import { fileNameFromContents, getCssSerialAssets } from './getCssDeps'; 16import { SerialAsset } from './serializerAssets'; 17 18export type Serializer = NonNullable<SerializerConfigT['customSerializer']>; 19 20export type SerializerParameters = Parameters<Serializer>; 21 22// A serializer that processes the input and returns a modified version. 23// Unlike a serializer, these can be chained together. 24export type SerializerPlugin = (...props: SerializerParameters) => SerializerParameters; 25 26export function withExpoSerializers(config: InputConfigT): InputConfigT { 27 const processors: SerializerPlugin[] = []; 28 if (!env.EXPO_NO_CLIENT_ENV_VARS) { 29 processors.push(environmentVariableSerializerPlugin); 30 } 31 32 return withSerializerPlugins(config, processors); 33} 34 35// There can only be one custom serializer as the input doesn't match the output. 36// Here we simply run 37export function withSerializerPlugins( 38 config: InputConfigT, 39 processors: SerializerPlugin[] 40): InputConfigT { 41 const originalSerializer = config.serializer?.customSerializer; 42 43 return { 44 ...config, 45 serializer: { 46 ...config.serializer, 47 customSerializer: createSerializerFromSerialProcessors(processors, originalSerializer), 48 }, 49 }; 50} 51 52function getDefaultSerializer(fallbackSerializer?: Serializer | null): Serializer { 53 const defaultSerializer = 54 fallbackSerializer ?? 55 (async (...params: SerializerParameters) => { 56 const bundle = baseJSBundle(...params); 57 const outputCode = bundleToString(bundle).code; 58 return outputCode; 59 }); 60 return async ( 61 ...props: SerializerParameters 62 ): Promise<string | { code: string; map: string }> => { 63 const [, , graph, options] = props; 64 const jsCode = await defaultSerializer(...props); 65 66 if (!options.sourceUrl) { 67 return jsCode; 68 } 69 const sourceUrl = isJscSafeUrl(options.sourceUrl) 70 ? toNormalUrl(options.sourceUrl) 71 : options.sourceUrl; 72 const url = new URL(sourceUrl, 'https://expo.dev'); 73 if ( 74 url.searchParams.get('platform') !== 'web' || 75 url.searchParams.get('serializer.output') !== 'static' 76 ) { 77 // Default behavior if `serializer.output=static` is not present in the URL. 78 return jsCode; 79 } 80 81 const cssDeps = getCssSerialAssets<MixedOutput>(graph.dependencies, { 82 projectRoot: options.projectRoot, 83 processModuleFilter: options.processModuleFilter, 84 }); 85 86 let jsAsset: SerialAsset | undefined; 87 88 if (jsCode) { 89 const stringContents = typeof jsCode === 'string' ? jsCode : jsCode.code; 90 jsAsset = { 91 filename: options.dev 92 ? 'index.js' 93 : `_expo/static/js/web/${fileNameFromContents({ 94 filepath: url.pathname, 95 src: stringContents, 96 })}.js`, 97 originFilename: 'index.js', 98 type: 'js', 99 metadata: {}, 100 source: stringContents, 101 }; 102 } 103 104 return JSON.stringify([jsAsset, ...cssDeps]); 105 }; 106} 107 108export function createSerializerFromSerialProcessors( 109 processors: (SerializerPlugin | undefined)[], 110 originalSerializer?: Serializer | null 111): Serializer { 112 const finalSerializer = getDefaultSerializer(originalSerializer); 113 return (...props: SerializerParameters): ReturnType<Serializer> => { 114 for (const processor of processors) { 115 if (processor) { 116 props = processor(...props); 117 } 118 } 119 120 return finalSerializer(...props); 121 }; 122} 123 124export { SerialAsset }; 125