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