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 const environment = options.customTransformOptions?.environment; 45 46 if ( 47 environment === 'client' && 48 // TODO: Ensure this works with windows. 49 // TODO: Add +api files. 50 filename.match(new RegExp(`^app/\\+html(\\.${options.platform})?\\.([tj]sx?|[cm]js)?$`)) 51 ) { 52 // Remove the server-only +html file from the bundle when bundling for a client environment. 53 return worker.transform( 54 config, 55 projectRoot, 56 filename, 57 !options.minify 58 ? Buffer.from( 59 // Use a string so this notice is visible in the bundle if the user is 60 // looking for it. 61 '"> The server-only +html file was removed from the client JS bundle by Expo CLI."' 62 ) 63 : Buffer.from(''), 64 options 65 ); 66 } 67 68 return worker.transform(config, projectRoot, filename, data, options); 69 } 70 71 // If the platform is not web, then return an empty module. 72 if (options.platform !== 'web') { 73 const code = matchCssModule(filename) ? 'module.exports={ unstable_styles: {} };' : ''; 74 return worker.transform( 75 config, 76 projectRoot, 77 filename, 78 // TODO: Native CSS Modules 79 Buffer.from(code), 80 options 81 ); 82 } 83 84 let code = data.toString('utf8'); 85 86 // Apply postcss transforms 87 code = await transformPostCssModule(projectRoot, { 88 src: code, 89 filename, 90 }); 91 92 // TODO: When native has CSS support, this will need to move higher up. 93 const syntax = matchSass(filename); 94 if (syntax) { 95 code = compileSass(projectRoot, { filename, src: code }, { syntax }).src; 96 } 97 98 // If the file is a CSS Module, then transform it to a JS module 99 // in development and a static CSS file in production. 100 if (matchCssModule(filename)) { 101 const results = await transformCssModuleWeb({ 102 filename, 103 src: code, 104 options: { 105 projectRoot, 106 dev: options.dev, 107 minify: options.minify, 108 sourceMap: false, 109 }, 110 }); 111 112 const jsModuleResults = await worker.transform( 113 config, 114 projectRoot, 115 filename, 116 Buffer.from(results.output), 117 options 118 ); 119 120 const cssCode = results.css.toString(); 121 const output: JsOutput[] = [ 122 { 123 type: 'js/module', 124 data: { 125 // @ts-expect-error 126 ...jsModuleResults.output[0]?.data, 127 128 // Append additional css metadata for static extraction. 129 css: { 130 code: cssCode, 131 lineCount: countLines(cssCode), 132 map: [], 133 functionMap: null, 134 }, 135 }, 136 }, 137 ]; 138 139 return { 140 dependencies: jsModuleResults.dependencies, 141 output, 142 }; 143 } 144 145 // Global CSS: 146 147 const { transform } = await import('lightningcss'); 148 149 // TODO: Add bundling to resolve imports 150 // https://lightningcss.dev/bundling.html#bundling-order 151 152 const cssResults = transform({ 153 filename, 154 code: Buffer.from(code), 155 sourceMap: false, 156 cssModules: false, 157 projectRoot, 158 minify: options.minify, 159 }); 160 161 // TODO: Warnings: 162 // cssResults.warnings.forEach((warning) => { 163 // }); 164 165 // Create a mock JS module that exports an empty object, 166 // this ensures Metro dependency graph is correct. 167 const jsModuleResults = await worker.transform( 168 config, 169 projectRoot, 170 filename, 171 options.dev ? Buffer.from(wrapDevelopmentCSS({ src: code, filename })) : Buffer.from(''), 172 options 173 ); 174 175 const cssCode = cssResults.code.toString(); 176 177 // In production, we export the CSS as a string and use a special type to prevent 178 // it from being included in the JS bundle. We'll extract the CSS like an asset later 179 // and append it to the HTML bundle. 180 const output: JsOutput[] = [ 181 { 182 type: 'js/module', 183 data: { 184 // @ts-expect-error 185 ...jsModuleResults.output[0]?.data, 186 187 // Append additional css metadata for static extraction. 188 css: { 189 code: cssCode, 190 lineCount: countLines(cssCode), 191 map: [], 192 functionMap: null, 193 }, 194 }, 195 }, 196 ]; 197 198 return { 199 dependencies: jsModuleResults.dependencies, 200 output, 201 }; 202} 203 204/** 205 * A custom Metro transformer that adds support for processing Expo-specific bundler features. 206 * - Global CSS files on web. 207 * - CSS Modules on web. 208 * - TODO: Tailwind CSS on web. 209 */ 210module.exports = { 211 // Use defaults for everything that's not custom. 212 ...worker, 213 transform, 214}; 215