19580591fSEvan Baconimport { MetroConfig } from '@expo/metro-config';
29580591fSEvan Baconimport crypto from 'crypto';
39580591fSEvan Baconimport type { Module } from 'metro';
49580591fSEvan Baconimport { getJsOutput, isJsModule } from 'metro/src/DeltaBundler/Serializers/helpers/js';
5*da5824c9SKudo Chienimport type { ReadOnlyDependencies } from 'metro/src/DeltaBundler/types';
69580591fSEvan Baconimport type IncrementalBundler from 'metro/src/IncrementalBundler';
79580591fSEvan Baconimport splitBundleOptions from 'metro/src/lib/splitBundleOptions';
89580591fSEvan Baconimport path from 'path';
99580591fSEvan Bacon
109580591fSEvan Bacon// import { getAssetData } from 'metro/src/Assets';
119580591fSEvan Bacon
129580591fSEvan Bacontype Options = {
139580591fSEvan Bacon  processModuleFilter: (modules: Module) => boolean;
149580591fSEvan Bacon  assetPlugins: readonly string[];
159580591fSEvan Bacon  platform?: string | null;
169580591fSEvan Bacon  projectRoot: string;
179580591fSEvan Bacon  publicPath: string;
189580591fSEvan Bacon};
199580591fSEvan Bacon
209580591fSEvan Bacontype MetroModuleCSSMetadata = {
219580591fSEvan Bacon  code: string;
229580591fSEvan Bacon  lineCount: number;
239580591fSEvan Bacon  map: any[];
249580591fSEvan Bacon};
259580591fSEvan Bacon
269580591fSEvan Baconexport type CSSAsset = {
279580591fSEvan Bacon  // 'styles.css'
289580591fSEvan Bacon  originFilename: string;
299580591fSEvan Bacon  // '_expo/static/css/bc6aa0a69dcebf8e8cac1faa76705756.css'
309580591fSEvan Bacon  filename: string;
319580591fSEvan Bacon  // '\ndiv {\n    background: cyan;\n}\n\n'
329580591fSEvan Bacon  source: string;
339580591fSEvan Bacon};
349580591fSEvan Bacon
359580591fSEvan Bacon// s = static
369580591fSEvan Baconconst STATIC_EXPORT_DIRECTORY = '_expo/static/css';
379580591fSEvan Bacon
389580591fSEvan Bacon/** @returns the static CSS assets used in a given bundle. CSS assets are only enabled if the `@expo/metro-config` `transformerPath` is used. */
399580591fSEvan Baconexport async function getCssModulesFromBundler(
409580591fSEvan Bacon  config: MetroConfig,
419580591fSEvan Bacon  incrementalBundler: IncrementalBundler,
429580591fSEvan Bacon  options: any
439580591fSEvan Bacon): Promise<CSSAsset[]> {
449580591fSEvan Bacon  // Static CSS is a web-only feature.
459580591fSEvan Bacon  if (options.platform !== 'web') {
469580591fSEvan Bacon    return [];
479580591fSEvan Bacon  }
489580591fSEvan Bacon
499580591fSEvan Bacon  const { entryFile, onProgress, resolverOptions, transformOptions } = splitBundleOptions(options);
509580591fSEvan Bacon
519580591fSEvan Bacon  const dependencies = await incrementalBundler.getDependencies(
529580591fSEvan Bacon    [entryFile],
539580591fSEvan Bacon    transformOptions,
549580591fSEvan Bacon    resolverOptions,
559580591fSEvan Bacon    { onProgress, shallow: false }
569580591fSEvan Bacon  );
579580591fSEvan Bacon
589580591fSEvan Bacon  return getCssModules(dependencies, {
599580591fSEvan Bacon    processModuleFilter: config.serializer.processModuleFilter,
609580591fSEvan Bacon    assetPlugins: config.transformer.assetPlugins,
619580591fSEvan Bacon    platform: transformOptions.platform,
629580591fSEvan Bacon    projectRoot: config.server.unstable_serverRoot ?? config.projectRoot,
639580591fSEvan Bacon    publicPath: config.transformer.publicPath,
649580591fSEvan Bacon  });
659580591fSEvan Bacon}
669580591fSEvan Bacon
679580591fSEvan Baconfunction hashString(str: string) {
689580591fSEvan Bacon  return crypto.createHash('md5').update(str).digest('hex');
699580591fSEvan Bacon}
709580591fSEvan Bacon
719580591fSEvan Baconfunction getCssModules(
729580591fSEvan Bacon  dependencies: ReadOnlyDependencies,
739580591fSEvan Bacon  { processModuleFilter, projectRoot }: Options
749580591fSEvan Bacon) {
759580591fSEvan Bacon  const promises = [];
769580591fSEvan Bacon
779580591fSEvan Bacon  for (const module of dependencies.values()) {
789580591fSEvan Bacon    if (
799580591fSEvan Bacon      isJsModule(module) &&
809580591fSEvan Bacon      processModuleFilter(module) &&
819580591fSEvan Bacon      getJsOutput(module).type === 'js/module' &&
829580591fSEvan Bacon      path.relative(projectRoot, module.path) !== 'package.json'
839580591fSEvan Bacon    ) {
849580591fSEvan Bacon      const cssMetadata = getCssMetadata(module);
859580591fSEvan Bacon      if (cssMetadata) {
869580591fSEvan Bacon        const contents = cssMetadata.code;
879580591fSEvan Bacon        const filename = path.join(
889580591fSEvan Bacon          // Consistent location
899580591fSEvan Bacon          STATIC_EXPORT_DIRECTORY,
909580591fSEvan Bacon          // Hashed file contents + name for caching
919580591fSEvan Bacon          getFileName(module.path) + '-' + hashString(module.path + contents) + '.css'
929580591fSEvan Bacon        );
939580591fSEvan Bacon        promises.push({
949580591fSEvan Bacon          originFilename: path.relative(projectRoot, module.path),
959580591fSEvan Bacon          filename,
969580591fSEvan Bacon          source: contents,
979580591fSEvan Bacon        });
989580591fSEvan Bacon      }
999580591fSEvan Bacon    }
1009580591fSEvan Bacon  }
1019580591fSEvan Bacon
1029580591fSEvan Bacon  return promises;
1039580591fSEvan Bacon}
1049580591fSEvan Bacon
1059580591fSEvan Baconfunction getCssMetadata(module: Module): MetroModuleCSSMetadata | null {
1069580591fSEvan Bacon  const data = module.output[0]?.data;
1079580591fSEvan Bacon  if (data && typeof data === 'object' && 'css' in data) {
1089580591fSEvan Bacon    if (typeof data.css !== 'object' || !('code' in (data as any).css)) {
1099580591fSEvan Bacon      throw new Error(
1109580591fSEvan Bacon        `Unexpected CSS metadata in Metro module (${module.path}): ${JSON.stringify(data.css)}`
1119580591fSEvan Bacon      );
1129580591fSEvan Bacon    }
1139580591fSEvan Bacon    return data.css as MetroModuleCSSMetadata;
1149580591fSEvan Bacon  }
1159580591fSEvan Bacon  return null;
1169580591fSEvan Bacon}
1179580591fSEvan Bacon
1189580591fSEvan Baconexport function getFileName(module: string) {
1199580591fSEvan Bacon  return path.basename(module).replace(/\.[^.]+$/, '');
1209580591fSEvan Bacon}
121