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 { Module, MixedOutput } from 'metro'; 9import baseJSBundle from 'metro/src/DeltaBundler/Serializers/baseJSBundle'; 10// @ts-expect-error 11import sourceMapString from 'metro/src/DeltaBundler/Serializers/sourceMapString'; 12import bundleToString from 'metro/src/lib/bundleToString'; 13import { InputConfigT, SerializerConfigT } from 'metro-config'; 14import path from 'path'; 15 16import { 17 serverPreludeSerializerPlugin, 18 environmentVariableSerializerPlugin, 19} from './environmentVariableSerializerPlugin'; 20import { fileNameFromContents, getCssSerialAssets } from './getCssDeps'; 21import { SerialAsset } from './serializerAssets'; 22import { env } from '../env'; 23 24export type Serializer = NonNullable<SerializerConfigT['customSerializer']>; 25 26export type SerializerParameters = Parameters<Serializer>; 27 28// A serializer that processes the input and returns a modified version. 29// Unlike a serializer, these can be chained together. 30export type SerializerPlugin = (...props: SerializerParameters) => SerializerParameters; 31 32export function withExpoSerializers(config: InputConfigT): InputConfigT { 33 const processors: SerializerPlugin[] = []; 34 processors.push(serverPreludeSerializerPlugin); 35 if (!env.EXPO_NO_CLIENT_ENV_VARS) { 36 processors.push(environmentVariableSerializerPlugin); 37 } 38 39 return withSerializerPlugins(config, processors); 40} 41 42// There can only be one custom serializer as the input doesn't match the output. 43// Here we simply run 44export function withSerializerPlugins( 45 config: InputConfigT, 46 processors: SerializerPlugin[] 47): InputConfigT { 48 const originalSerializer = config.serializer?.customSerializer; 49 50 return { 51 ...config, 52 serializer: { 53 ...config.serializer, 54 customSerializer: createSerializerFromSerialProcessors(processors, originalSerializer), 55 }, 56 }; 57} 58 59function getDefaultSerializer(fallbackSerializer?: Serializer | null): Serializer { 60 const defaultSerializer = 61 fallbackSerializer ?? 62 (async (...params: SerializerParameters) => { 63 const bundle = baseJSBundle(...params); 64 const outputCode = bundleToString(bundle).code; 65 return outputCode; 66 }); 67 return async ( 68 ...props: SerializerParameters 69 ): Promise<string | { code: string; map: string }> => { 70 const [entryPoint, preModules, graph, options] = props; 71 72 const jsCode = await defaultSerializer(entryPoint, preModules, graph, options); 73 74 if (!options.sourceUrl) { 75 return jsCode; 76 } 77 const sourceUrl = isJscSafeUrl(options.sourceUrl) 78 ? toNormalUrl(options.sourceUrl) 79 : options.sourceUrl; 80 const url = new URL(sourceUrl, 'https://expo.dev'); 81 if ( 82 url.searchParams.get('platform') !== 'web' || 83 url.searchParams.get('serializer.output') !== 'static' 84 ) { 85 // Default behavior if `serializer.output=static` is not present in the URL. 86 return jsCode; 87 } 88 89 const includeSourceMaps = url.searchParams.get('serializer.map') === 'true'; 90 91 const cssDeps = getCssSerialAssets<MixedOutput>(graph.dependencies, { 92 projectRoot: options.projectRoot, 93 processModuleFilter: options.processModuleFilter, 94 }); 95 96 const jsAssets: SerialAsset[] = []; 97 98 if (jsCode) { 99 const stringContents = typeof jsCode === 'string' ? jsCode : jsCode.code; 100 const jsFilename = fileNameFromContents({ 101 filepath: url.pathname, 102 src: stringContents, 103 }); 104 jsAssets.push({ 105 filename: options.dev ? 'index.js' : `_expo/static/js/web/${jsFilename}.js`, 106 originFilename: 'index.js', 107 type: 'js', 108 metadata: {}, 109 source: stringContents, 110 }); 111 112 if ( 113 // Only include the source map if the `options.sourceMapUrl` option is provided and we are exporting a static build. 114 includeSourceMaps && 115 options.sourceMapUrl 116 ) { 117 const sourceMap = typeof jsCode === 'string' ? serializeToSourceMap(...props) : jsCode.map; 118 119 // Make all paths relative to the server root to prevent the entire user filesystem from being exposed. 120 const parsed = JSON.parse(sourceMap); 121 // TODO: Maybe we can do this earlier. 122 parsed.sources = parsed.sources.map( 123 // TODO: Maybe basePath support 124 (value: string) => { 125 if (value.startsWith('/')) { 126 return '/' + path.relative(options.serverRoot ?? options.projectRoot, value); 127 } 128 // Prevent `__prelude__` from being relative. 129 return value; 130 } 131 ); 132 133 jsAssets.push({ 134 filename: options.dev ? 'index.map' : `_expo/static/js/web/${jsFilename}.js.map`, 135 originFilename: 'index.map', 136 type: 'map', 137 metadata: {}, 138 source: JSON.stringify(parsed), 139 }); 140 } 141 } 142 143 return JSON.stringify([...jsAssets, ...cssDeps]); 144 }; 145} 146 147function getSortedModules( 148 graph: SerializerParameters[2], 149 { 150 createModuleId, 151 }: { 152 createModuleId: (path: string) => number; 153 } 154): readonly Module<any>[] { 155 const modules = [...graph.dependencies.values()]; 156 // Assign IDs to modules in a consistent order 157 for (const module of modules) { 158 createModuleId(module.path); 159 } 160 // Sort by IDs 161 return modules.sort( 162 (a: Module<any>, b: Module<any>) => createModuleId(a.path) - createModuleId(b.path) 163 ); 164} 165 166function serializeToSourceMap(...props: SerializerParameters): string { 167 const [, prepend, graph, options] = props; 168 169 const modules = [ 170 ...prepend, 171 ...getSortedModules(graph, { 172 createModuleId: options.createModuleId, 173 }), 174 ]; 175 176 return sourceMapString(modules, { 177 ...options, 178 }); 179} 180 181export function createSerializerFromSerialProcessors( 182 processors: (SerializerPlugin | undefined)[], 183 originalSerializer?: Serializer | null 184): Serializer { 185 const finalSerializer = getDefaultSerializer(originalSerializer); 186 return (...props: SerializerParameters): ReturnType<Serializer> => { 187 for (const processor of processors) { 188 if (processor) { 189 props = processor(...props); 190 } 191 } 192 193 return finalSerializer(...props); 194 }; 195} 196 197export { SerialAsset }; 198