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