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