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