1/** 2 * Copyright 2023-present 650 Industries (Expo). All rights reserved. 3 * Copyright (c) Meta Platforms, Inc. and affiliates. 4 * 5 * This source code is licensed under the MIT license found in the 6 * LICENSE file in the root directory of this source tree. 7 */ 8import { FBSourceFunctionMap, MetroSourceMapSegmentTuple } from 'metro-source-map'; 9import worker, { 10 JsTransformerConfig, 11 JsTransformOptions, 12 TransformResponse, 13} from 'metro-transform-worker'; 14 15import { wrapDevelopmentCSS } from './css'; 16import { matchCssModule, transformCssModuleWeb } from './css-modules'; 17import { transformPostCssModule } from './postcss'; 18import { compileSass, matchSass } from './sass'; 19 20const countLines = require('metro/src/lib/countLines') as (string: string) => number; 21 22type JSFileType = 'js/script' | 'js/module' | 'js/module/asset'; 23 24type JsOutput = { 25 data: { 26 code: string; 27 lineCount: number; 28 map: MetroSourceMapSegmentTuple[]; 29 functionMap: FBSourceFunctionMap | null; 30 }; 31 type: JSFileType; 32}; 33 34export async function transform( 35 config: JsTransformerConfig, 36 projectRoot: string, 37 filename: string, 38 data: Buffer, 39 options: JsTransformOptions 40): Promise<TransformResponse> { 41 const isCss = options.type !== 'asset' && /\.(s?css|sass)$/.test(filename); 42 // If the file is not CSS, then use the default behavior. 43 if (!isCss) { 44 return worker.transform(config, projectRoot, filename, data, options); 45 } 46 47 // If the platform is not web, then return an empty module. 48 if (options.platform !== 'web') { 49 const code = matchCssModule(filename) ? 'module.exports={};' : ''; 50 return worker.transform( 51 config, 52 projectRoot, 53 filename, 54 // TODO: Native CSS Modules 55 Buffer.from(code), 56 options 57 ); 58 } 59 60 let code = data.toString('utf8'); 61 62 // Apply postcss transforms 63 code = await transformPostCssModule(projectRoot, { 64 src: code, 65 filename, 66 }); 67 68 // TODO: When native has CSS support, this will need to move higher up. 69 const syntax = matchSass(filename); 70 if (syntax) { 71 code = compileSass(projectRoot, { filename, src: code }, { syntax }).src; 72 } 73 74 // If the file is a CSS Module, then transform it to a JS module 75 // in development and a static CSS file in production. 76 if (matchCssModule(filename)) { 77 const results = await transformCssModuleWeb({ 78 filename, 79 src: code, 80 options: { 81 projectRoot, 82 dev: options.dev, 83 minify: options.minify, 84 sourceMap: false, 85 }, 86 }); 87 88 const jsModuleResults = await worker.transform( 89 config, 90 projectRoot, 91 filename, 92 Buffer.from(results.output), 93 options 94 ); 95 96 const cssCode = results.css.toString(); 97 const output: JsOutput[] = [ 98 { 99 type: 'js/module', 100 data: { 101 // @ts-expect-error 102 ...jsModuleResults.output[0]?.data, 103 104 // Append additional css metadata for static extraction. 105 css: { 106 code: cssCode, 107 lineCount: countLines(cssCode), 108 map: [], 109 functionMap: null, 110 }, 111 }, 112 }, 113 ]; 114 115 return { 116 dependencies: jsModuleResults.dependencies, 117 output, 118 }; 119 } 120 121 // Global CSS: 122 123 const { transform } = await import('lightningcss'); 124 125 // TODO: Add bundling to resolve imports 126 // https://lightningcss.dev/bundling.html#bundling-order 127 128 const cssResults = transform({ 129 filename, 130 code: Buffer.from(code), 131 sourceMap: false, 132 cssModules: false, 133 projectRoot, 134 minify: options.minify, 135 }); 136 137 // TODO: Warnings: 138 // cssResults.warnings.forEach((warning) => { 139 // }); 140 141 // Create a mock JS module that exports an empty object, 142 // this ensures Metro dependency graph is correct. 143 const jsModuleResults = await worker.transform( 144 config, 145 projectRoot, 146 filename, 147 options.dev ? Buffer.from(wrapDevelopmentCSS({ src: code, filename })) : Buffer.from(''), 148 options 149 ); 150 151 const cssCode = cssResults.code.toString(); 152 153 // In production, we export the CSS as a string and use a special type to prevent 154 // it from being included in the JS bundle. We'll extract the CSS like an asset later 155 // and append it to the HTML bundle. 156 const output: JsOutput[] = [ 157 { 158 type: 'js/module', 159 data: { 160 // @ts-expect-error 161 ...jsModuleResults.output[0]?.data, 162 163 // Append additional css metadata for static extraction. 164 css: { 165 code: cssCode, 166 lineCount: countLines(cssCode), 167 map: [], 168 functionMap: null, 169 }, 170 }, 171 }, 172 ]; 173 174 return { 175 dependencies: jsModuleResults.dependencies, 176 output, 177 }; 178} 179 180/** 181 * A custom Metro transformer that adds support for processing Expo-specific bundler features. 182 * - Global CSS files on web. 183 * - CSS Modules on web. 184 * - TODO: Tailwind CSS on web. 185 */ 186module.exports = { 187 // Use defaults for everything that's not custom. 188 ...worker, 189 transform, 190}; 191