19b2597baSEvan Bacon/** 29b2597baSEvan Bacon * Copyright 2023-present 650 Industries (Expo). All rights reserved. 39b2597baSEvan Bacon * Copyright (c) Meta Platforms, Inc. and affiliates. 49b2597baSEvan Bacon * 59b2597baSEvan Bacon * This source code is licensed under the MIT license found in the 69b2597baSEvan Bacon * LICENSE file in the root directory of this source tree. 79b2597baSEvan Bacon */ 89b2597baSEvan Baconimport { FBSourceFunctionMap, MetroSourceMapSegmentTuple } from 'metro-source-map'; 99b2597baSEvan Baconimport worker, { 109b2597baSEvan Bacon JsTransformerConfig, 119b2597baSEvan Bacon JsTransformOptions, 129b2597baSEvan Bacon TransformResponse, 139b2597baSEvan Bacon} from 'metro-transform-worker'; 149b2597baSEvan Bacon 159b2597baSEvan Baconimport { wrapDevelopmentCSS } from './css'; 169b2597baSEvan Baconimport { matchCssModule, transformCssModuleWeb } from './css-modules'; 17e671e832SEvan Baconimport { transformPostCssModule } from './postcss'; 187cebda5aSEvan Baconimport { compileSass, matchSass } from './sass'; 199b2597baSEvan Bacon 209b2597baSEvan Baconconst countLines = require('metro/src/lib/countLines') as (string: string) => number; 219b2597baSEvan Bacon 229b2597baSEvan Bacontype JSFileType = 'js/script' | 'js/module' | 'js/module/asset'; 239b2597baSEvan Bacon 249b2597baSEvan Bacontype JsOutput = { 259b2597baSEvan Bacon data: { 269b2597baSEvan Bacon code: string; 279b2597baSEvan Bacon lineCount: number; 289b2597baSEvan Bacon map: MetroSourceMapSegmentTuple[]; 299b2597baSEvan Bacon functionMap: FBSourceFunctionMap | null; 309b2597baSEvan Bacon }; 319b2597baSEvan Bacon type: JSFileType; 329b2597baSEvan Bacon}; 339b2597baSEvan Bacon 349b2597baSEvan Baconexport async function transform( 359b2597baSEvan Bacon config: JsTransformerConfig, 369b2597baSEvan Bacon projectRoot: string, 379b2597baSEvan Bacon filename: string, 389b2597baSEvan Bacon data: Buffer, 399b2597baSEvan Bacon options: JsTransformOptions 409b2597baSEvan Bacon): Promise<TransformResponse> { 417cebda5aSEvan Bacon const isCss = options.type !== 'asset' && /\.(s?css|sass)$/.test(filename); 429b2597baSEvan Bacon // If the file is not CSS, then use the default behavior. 439b2597baSEvan Bacon if (!isCss) { 448455e99cSEvan Bacon const environment = options.customTransformOptions?.environment; 458455e99cSEvan Bacon 468455e99cSEvan Bacon if ( 4746f023faSEvan Bacon environment !== 'node' && 488455e99cSEvan Bacon // TODO: Ensure this works with windows. 4946f023faSEvan Bacon (filename.match(new RegExp(`^app/\\+html(\\.${options.platform})?\\.([tj]sx?|[cm]js)?$`)) || 5046f023faSEvan Bacon // Strip +api files. 5146f023faSEvan Bacon filename.match(/\+api(\.(native|ios|android|web))?\.[tj]sx?$/)) 528455e99cSEvan Bacon ) { 5346f023faSEvan Bacon // Remove the server-only +html file and API Routes from the bundle when bundling for a client environment. 548455e99cSEvan Bacon return worker.transform( 558455e99cSEvan Bacon config, 568455e99cSEvan Bacon projectRoot, 578455e99cSEvan Bacon filename, 588455e99cSEvan Bacon !options.minify 598455e99cSEvan Bacon ? Buffer.from( 608455e99cSEvan Bacon // Use a string so this notice is visible in the bundle if the user is 618455e99cSEvan Bacon // looking for it. 6246f023faSEvan Bacon '"> The server-only file was removed from the client JS bundle by Expo CLI."' 638455e99cSEvan Bacon ) 648455e99cSEvan Bacon : Buffer.from(''), 658455e99cSEvan Bacon options 668455e99cSEvan Bacon ); 678455e99cSEvan Bacon } 688455e99cSEvan Bacon 6946f023faSEvan Bacon if ( 7046f023faSEvan Bacon environment !== 'node' && 7146f023faSEvan Bacon !filename.match(/\/node_modules\//) && 7246f023faSEvan Bacon filename.match(/\+api(\.(native|ios|android|web))?\.[tj]sx?$/) 7346f023faSEvan Bacon ) { 7446f023faSEvan Bacon // Clear the contents of +api files when bundling for the client. 7546f023faSEvan Bacon // This ensures that the client doesn't accidentally use the server-only +api files. 7646f023faSEvan Bacon return worker.transform(config, projectRoot, filename, Buffer.from(''), options); 7746f023faSEvan Bacon } 7846f023faSEvan Bacon 799b2597baSEvan Bacon return worker.transform(config, projectRoot, filename, data, options); 809b2597baSEvan Bacon } 819b2597baSEvan Bacon 829b2597baSEvan Bacon // If the platform is not web, then return an empty module. 839b2597baSEvan Bacon if (options.platform !== 'web') { 8440ab349aSEvan Bacon const code = matchCssModule(filename) ? 'module.exports={ unstable_styles: {} };' : ''; 859b2597baSEvan Bacon return worker.transform( 869b2597baSEvan Bacon config, 879b2597baSEvan Bacon projectRoot, 889b2597baSEvan Bacon filename, 899b2597baSEvan Bacon // TODO: Native CSS Modules 909b2597baSEvan Bacon Buffer.from(code), 919b2597baSEvan Bacon options 929b2597baSEvan Bacon ); 939b2597baSEvan Bacon } 949b2597baSEvan Bacon 957cebda5aSEvan Bacon let code = data.toString('utf8'); 967cebda5aSEvan Bacon 97e671e832SEvan Bacon // Apply postcss transforms 98e671e832SEvan Bacon code = await transformPostCssModule(projectRoot, { 99e671e832SEvan Bacon src: code, 100e671e832SEvan Bacon filename, 101e671e832SEvan Bacon }); 102e671e832SEvan Bacon 1037cebda5aSEvan Bacon // TODO: When native has CSS support, this will need to move higher up. 1047cebda5aSEvan Bacon const syntax = matchSass(filename); 1057cebda5aSEvan Bacon if (syntax) { 1067cebda5aSEvan Bacon code = compileSass(projectRoot, { filename, src: code }, { syntax }).src; 1077cebda5aSEvan Bacon } 1089b2597baSEvan Bacon 1099b2597baSEvan Bacon // If the file is a CSS Module, then transform it to a JS module 1109b2597baSEvan Bacon // in development and a static CSS file in production. 1119b2597baSEvan Bacon if (matchCssModule(filename)) { 1129b2597baSEvan Bacon const results = await transformCssModuleWeb({ 1139b2597baSEvan Bacon filename, 1149b2597baSEvan Bacon src: code, 1159b2597baSEvan Bacon options: { 1169b2597baSEvan Bacon projectRoot, 1179b2597baSEvan Bacon dev: options.dev, 1189b2597baSEvan Bacon minify: options.minify, 1199b2597baSEvan Bacon sourceMap: false, 1209b2597baSEvan Bacon }, 1219b2597baSEvan Bacon }); 1229b2597baSEvan Bacon 1239b2597baSEvan Bacon const jsModuleResults = await worker.transform( 1249b2597baSEvan Bacon config, 1259b2597baSEvan Bacon projectRoot, 1269b2597baSEvan Bacon filename, 1279b2597baSEvan Bacon Buffer.from(results.output), 1289b2597baSEvan Bacon options 1299b2597baSEvan Bacon ); 1309b2597baSEvan Bacon 1319b2597baSEvan Bacon const cssCode = results.css.toString(); 1329b2597baSEvan Bacon const output: JsOutput[] = [ 1339b2597baSEvan Bacon { 1349b2597baSEvan Bacon type: 'js/module', 1359b2597baSEvan Bacon data: { 1369b2597baSEvan Bacon // @ts-expect-error 1379580591fSEvan Bacon ...jsModuleResults.output[0]?.data, 1389b2597baSEvan Bacon 1399b2597baSEvan Bacon // Append additional css metadata for static extraction. 1409b2597baSEvan Bacon css: { 1419b2597baSEvan Bacon code: cssCode, 1429b2597baSEvan Bacon lineCount: countLines(cssCode), 1439b2597baSEvan Bacon map: [], 1449b2597baSEvan Bacon functionMap: null, 1459b2597baSEvan Bacon }, 1469b2597baSEvan Bacon }, 1479b2597baSEvan Bacon }, 1489b2597baSEvan Bacon ]; 1499b2597baSEvan Bacon 1509b2597baSEvan Bacon return { 1519b2597baSEvan Bacon dependencies: jsModuleResults.dependencies, 1529b2597baSEvan Bacon output, 1539b2597baSEvan Bacon }; 1549b2597baSEvan Bacon } 1559b2597baSEvan Bacon 1569b2597baSEvan Bacon // Global CSS: 1579b2597baSEvan Bacon 158*1a3a1db5SEvan Bacon const { transform } = require('lightningcss') as typeof import('lightningcss'); 1599b2597baSEvan Bacon 1609b2597baSEvan Bacon // TODO: Add bundling to resolve imports 1619b2597baSEvan Bacon // https://lightningcss.dev/bundling.html#bundling-order 1629b2597baSEvan Bacon 1639b2597baSEvan Bacon const cssResults = transform({ 1649b2597baSEvan Bacon filename, 1659b2597baSEvan Bacon code: Buffer.from(code), 1669b2597baSEvan Bacon sourceMap: false, 1679b2597baSEvan Bacon cssModules: false, 1689b2597baSEvan Bacon projectRoot, 1699b2597baSEvan Bacon minify: options.minify, 1709b2597baSEvan Bacon }); 1719b2597baSEvan Bacon 1729b2597baSEvan Bacon // TODO: Warnings: 1739b2597baSEvan Bacon // cssResults.warnings.forEach((warning) => { 1749b2597baSEvan Bacon // }); 1759b2597baSEvan Bacon 1769b2597baSEvan Bacon // Create a mock JS module that exports an empty object, 1779b2597baSEvan Bacon // this ensures Metro dependency graph is correct. 1789b2597baSEvan Bacon const jsModuleResults = await worker.transform( 1799b2597baSEvan Bacon config, 1809b2597baSEvan Bacon projectRoot, 1819b2597baSEvan Bacon filename, 1829580591fSEvan Bacon options.dev ? Buffer.from(wrapDevelopmentCSS({ src: code, filename })) : Buffer.from(''), 1839b2597baSEvan Bacon options 1849b2597baSEvan Bacon ); 1859b2597baSEvan Bacon 1869b2597baSEvan Bacon const cssCode = cssResults.code.toString(); 1879b2597baSEvan Bacon 1889b2597baSEvan Bacon // In production, we export the CSS as a string and use a special type to prevent 1899b2597baSEvan Bacon // it from being included in the JS bundle. We'll extract the CSS like an asset later 1909b2597baSEvan Bacon // and append it to the HTML bundle. 1919b2597baSEvan Bacon const output: JsOutput[] = [ 1929b2597baSEvan Bacon { 1939580591fSEvan Bacon type: 'js/module', 1949b2597baSEvan Bacon data: { 1959b2597baSEvan Bacon // @ts-expect-error 1969580591fSEvan Bacon ...jsModuleResults.output[0]?.data, 1979b2597baSEvan Bacon 1989b2597baSEvan Bacon // Append additional css metadata for static extraction. 1999b2597baSEvan Bacon css: { 2009b2597baSEvan Bacon code: cssCode, 2019b2597baSEvan Bacon lineCount: countLines(cssCode), 2029b2597baSEvan Bacon map: [], 2039b2597baSEvan Bacon functionMap: null, 2049b2597baSEvan Bacon }, 2059b2597baSEvan Bacon }, 2069b2597baSEvan Bacon }, 2079b2597baSEvan Bacon ]; 2089b2597baSEvan Bacon 2099b2597baSEvan Bacon return { 2109b2597baSEvan Bacon dependencies: jsModuleResults.dependencies, 2119b2597baSEvan Bacon output, 2129b2597baSEvan Bacon }; 2139b2597baSEvan Bacon} 2149b2597baSEvan Bacon 2159b2597baSEvan Bacon/** 2169b2597baSEvan Bacon * A custom Metro transformer that adds support for processing Expo-specific bundler features. 2179b2597baSEvan Bacon * - Global CSS files on web. 2189b2597baSEvan Bacon * - CSS Modules on web. 2199b2597baSEvan Bacon * - TODO: Tailwind CSS on web. 2209b2597baSEvan Bacon */ 2219b2597baSEvan Baconmodule.exports = { 2229b2597baSEvan Bacon // Use defaults for everything that's not custom. 2239b2597baSEvan Bacon ...worker, 2249b2597baSEvan Bacon transform, 2259b2597baSEvan Bacon}; 226