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