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 if (options.dev) { 89 // Dev has the CSS appended to the JS file. 90 return worker.transform(config, projectRoot, filename, Buffer.from(results.output), options); 91 } 92 93 const jsModuleResults = await worker.transform( 94 config, 95 projectRoot, 96 filename, 97 Buffer.from(results.output), 98 options 99 ); 100 101 const cssCode = results.css.toString(); 102 const output: JsOutput[] = [ 103 { 104 type: 'js/module', 105 data: { 106 // @ts-expect-error 107 ...jsModuleResults.output[0].data, 108 109 // Append additional css metadata for static extraction. 110 css: { 111 code: cssCode, 112 lineCount: countLines(cssCode), 113 map: [], 114 functionMap: null, 115 }, 116 }, 117 }, 118 ]; 119 120 return { 121 dependencies: jsModuleResults.dependencies, 122 output, 123 }; 124 } 125 126 // Global CSS: 127 128 if (options.dev) { 129 return worker.transform( 130 config, 131 projectRoot, 132 filename, 133 // In development, we use a JS file that appends a style tag to the 134 // document. This is necessary because we need to replace the style tag 135 // when the CSS changes. 136 // NOTE: We may change this to better support static rendering in the future. 137 Buffer.from(wrapDevelopmentCSS({ src: code, filename })), 138 options 139 ); 140 } 141 142 const { transform } = await import('lightningcss'); 143 144 // TODO: Add bundling to resolve imports 145 // https://lightningcss.dev/bundling.html#bundling-order 146 147 const cssResults = transform({ 148 filename, 149 code: Buffer.from(code), 150 sourceMap: false, 151 cssModules: false, 152 projectRoot, 153 minify: options.minify, 154 }); 155 156 // TODO: Warnings: 157 // cssResults.warnings.forEach((warning) => { 158 // }); 159 160 // Create a mock JS module that exports an empty object, 161 // this ensures Metro dependency graph is correct. 162 const jsModuleResults = await worker.transform( 163 config, 164 projectRoot, 165 filename, 166 Buffer.from(''), 167 options 168 ); 169 170 const cssCode = cssResults.code.toString(); 171 172 // In production, we export the CSS as a string and use a special type to prevent 173 // it from being included in the JS bundle. We'll extract the CSS like an asset later 174 // and append it to the HTML bundle. 175 const output: JsOutput[] = [ 176 { 177 data: { 178 // @ts-expect-error 179 ...jsModuleResults.output[0].data, 180 181 // Append additional css metadata for static extraction. 182 css: { 183 code: cssCode, 184 lineCount: countLines(cssCode), 185 map: [], 186 functionMap: null, 187 }, 188 }, 189 type: 'js/module', 190 }, 191 ]; 192 193 return { 194 dependencies: jsModuleResults.dependencies, 195 output, 196 }; 197} 198 199/** 200 * A custom Metro transformer that adds support for processing Expo-specific bundler features. 201 * - Global CSS files on web. 202 * - CSS Modules on web. 203 * - TODO: Tailwind CSS on web. 204 */ 205module.exports = { 206 // Use defaults for everything that's not custom. 207 ...worker, 208 transform, 209}; 210